├── .idea
├── .name
├── .gitignore
├── codeStyles
│ └── codeStyleConfig.xml
├── vcs.xml
├── compiler.xml
├── gradle.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── raw
│ │ │ └── alarm.mp3
│ │ ├── font
│ │ │ ├── cairo_bold.ttf
│ │ │ ├── cairo_regular.ttf
│ │ │ └── spartan_bold.ttf
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── fragment_maps.xml
│ │ │ └── alert.xml
│ │ ├── drawable
│ │ │ ├── cloud.xml
│ │ │ ├── visibility.xml
│ │ │ ├── wind_speed.xml
│ │ │ ├── cloudy_24.xml
│ │ │ ├── humidity.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── speedometer.xml
│ │ │ ├── storm.xml
│ │ │ ├── storm_24.xml
│ │ │ ├── rainy_24.xml
│ │ │ ├── clear_day.xml
│ │ │ ├── clear_day_24.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── rainy_day.xml
│ │ │ ├── rainy_day_24.xml
│ │ │ ├── cloudy_day.xml
│ │ │ ├── cloudy_day_24.xml
│ │ │ ├── cloudy_night.xml
│ │ │ ├── cloudy_night_24.xml
│ │ │ ├── foggy_day.xml
│ │ │ ├── foggy_day_24.xml
│ │ │ ├── clear_night.xml
│ │ │ └── clear_night_24.xml
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ └── values-night
│ │ │ └── themes.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── amrmsaraya
│ │ │ └── weather
│ │ │ ├── util
│ │ │ ├── UiState.kt
│ │ │ ├── enums
│ │ │ │ ├── Location.kt
│ │ │ │ ├── Language.kt
│ │ │ │ ├── WindSpeed.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Temperature.kt
│ │ │ ├── dispatchers
│ │ │ │ ├── IDispatchers.kt
│ │ │ │ └── IDispatchersImpl.kt
│ │ │ ├── ThrowableMessage.kt
│ │ │ ├── ForecastIcons.kt
│ │ │ ├── LocaleHelper.kt
│ │ │ ├── NotificationHelper.kt
│ │ │ ├── GeocoderHelper.kt
│ │ │ └── LocationHelper.kt
│ │ │ ├── presentation
│ │ │ ├── alerts
│ │ │ │ ├── AlertsUiState.kt
│ │ │ │ ├── AlertsIntent.kt
│ │ │ │ └── AlertsViewModel.kt
│ │ │ ├── map
│ │ │ │ ├── MapUiState.kt
│ │ │ │ ├── MapIntent.kt
│ │ │ │ └── MapViewModel.kt
│ │ │ ├── home
│ │ │ │ ├── HomeIntent.kt
│ │ │ │ └── HomeUiState.kt
│ │ │ ├── favorites
│ │ │ │ ├── FavoritesUiState.kt
│ │ │ │ ├── FavoritesIntent.kt
│ │ │ │ └── FavoritesViewModel.kt
│ │ │ ├── favorite_details
│ │ │ │ ├── FavoriteDetailsIntent.kt
│ │ │ │ ├── FavoriteDetailsUiState.kt
│ │ │ │ ├── FavoriteDetailsViewModel.kt
│ │ │ │ └── FavoriteDetailsScreen.kt
│ │ │ ├── theme
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Color.kt
│ │ │ │ └── Type.kt
│ │ │ ├── settings
│ │ │ │ └── SettingsViewModel.kt
│ │ │ └── navigation
│ │ │ │ └── Screens.kt
│ │ │ ├── di
│ │ │ ├── Application.kt
│ │ │ ├── Dispatcher.kt
│ │ │ ├── DataStore.kt
│ │ │ ├── Room.kt
│ │ │ ├── usecase
│ │ │ │ ├── AlertModule.kt
│ │ │ │ ├── PreferenceModule.kt
│ │ │ │ └── ForecastModule.kt
│ │ │ ├── DataSource.kt
│ │ │ ├── Repository.kt
│ │ │ └── Ktor.kt
│ │ │ └── service
│ │ │ ├── AlertWorker.kt
│ │ │ └── AlertService.kt
│ │ └── AndroidManifest.xml
└── proguard-rules.pro
├── data
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── amrmsaraya
│ │ │ └── weather
│ │ │ └── data
│ │ │ ├── source
│ │ │ ├── RemoteDataSource.kt
│ │ │ ├── AlertDataSource.kt
│ │ │ └── LocalDataSource.kt
│ │ │ ├── model
│ │ │ ├── ForecastRequest.kt
│ │ │ ├── forecast
│ │ │ │ ├── FeelsLikeDTO.kt
│ │ │ │ ├── WeatherDTO.kt
│ │ │ │ ├── TempDTO.kt
│ │ │ │ ├── AlertDTO.kt
│ │ │ │ ├── CurrentDTO.kt
│ │ │ │ ├── HourlyDTO.kt
│ │ │ │ ├── ForecastDTO.kt
│ │ │ │ └── DailyDTO.kt
│ │ │ └── AlertsDTO.kt
│ │ │ ├── remote
│ │ │ └── ApiService.kt
│ │ │ ├── sourceImp
│ │ │ ├── RemoteDataSourceImp.kt
│ │ │ ├── AlertDataSourceImp.kt
│ │ │ └── LocalDataSourceImp.kt
│ │ │ ├── mapper
│ │ │ └── AlertsMapper.kt
│ │ │ ├── local
│ │ │ ├── AlertDao.kt
│ │ │ ├── WeatherDao.kt
│ │ │ └── WeatherDatabase.kt
│ │ │ ├── repositoryImp
│ │ │ ├── AlertRepoImp.kt
│ │ │ ├── ForecastRepoImp.kt
│ │ │ └── PreferencesRepoImp.kt
│ │ │ └── util
│ │ │ └── Converter.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── amrmsaraya
│ │ │ └── weather
│ │ │ └── data
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── amrmsaraya
│ │ └── weather
│ │ └── data
│ │ └── local
│ │ └── AlertDaoTest.kt
├── proguard-rules.pro
└── build.gradle
├── domain
├── .gitignore
├── src
│ ├── main
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── amrmsaraya
│ │ │ └── weather
│ │ │ └── domain
│ │ │ ├── model
│ │ │ ├── forecast
│ │ │ │ ├── Weather.kt
│ │ │ │ ├── FeelsLike.kt
│ │ │ │ ├── Temp.kt
│ │ │ │ ├── Alert.kt
│ │ │ │ ├── Forecast.kt
│ │ │ │ ├── Current.kt
│ │ │ │ ├── Hourly.kt
│ │ │ │ └── Daily.kt
│ │ │ ├── Alerts.kt
│ │ │ ├── ForecastRequest.kt
│ │ │ └── Settings.kt
│ │ │ ├── util
│ │ │ └── Response.kt
│ │ │ ├── usecase
│ │ │ ├── alert
│ │ │ │ ├── InsertAlert.kt
│ │ │ │ ├── GetAlert.kt
│ │ │ │ ├── GetAlerts.kt
│ │ │ │ └── DeleteAlert.kt
│ │ │ ├── forecast
│ │ │ │ ├── InsertForecast.kt
│ │ │ │ ├── GetFavoriteForecasts.kt
│ │ │ │ ├── DeleteForecast.kt
│ │ │ │ ├── GetForecastFromMap.kt
│ │ │ │ ├── UpdateFavoritesForecast.kt
│ │ │ │ ├── GetForecast.kt
│ │ │ │ └── GetCurrentForecast.kt
│ │ │ └── preferences
│ │ │ │ ├── GetIntPreference.kt
│ │ │ │ ├── GetStringPreference.kt
│ │ │ │ ├── GetBooleanPreference.kt
│ │ │ │ ├── SetDefaultPreferences.kt
│ │ │ │ ├── RestorePreferences.kt
│ │ │ │ └── SavePreference.kt
│ │ │ └── repository
│ │ │ ├── AlertRepo.kt
│ │ │ ├── PreferencesRepo.kt
│ │ │ └── ForecastRepo.kt
│ └── test
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── amrmsaraya
│ │ └── weather
│ │ └── domain
│ │ ├── repository
│ │ ├── FakeAlertRepo.kt
│ │ └── FakeForecastRepo.kt
│ │ └── usecase
│ │ ├── alert
│ │ ├── InsertAlertTest.kt
│ │ ├── GetAlertTest.kt
│ │ └── DeleteAlertTest.kt
│ │ └── forecast
│ │ ├── GetForecastFromMapTest.kt
│ │ ├── GetFavoriteForecastsTest.kt
│ │ ├── UpdateFavoritesForecastTest.kt
│ │ ├── InsertForecastTest.kt
│ │ ├── GetForecastTest.kt
│ │ ├── DeleteForecastTest.kt
│ │ └── GetCurrentForecastTest.kt
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradlew.bat
└── README.md
/.idea/.name:
--------------------------------------------------------------------------------
1 | Weather
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/alarm.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/raw/alarm.mp3
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/cairo_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/font/cairo_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/font/cairo_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/font/cairo_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/spartan_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/font/spartan_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amrmsaraya/weather/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/UiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | data class UiState(
4 | val data: T? = null,
5 | val throwable: Throwable? = null,
6 | val isLoading: Boolean = false
7 | )
8 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 10 11:36:46 EET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Weather.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Weather(
4 | val id: Int = 0,
5 | val main: String = "",
6 | val description: String = "",
7 | val icon: String = ""
8 | )
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/FeelsLike.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class FeelsLike(
4 | val day: Double = 0.0,
5 | val night: Double = 0.0,
6 | val eve: Double = 0.0,
7 | val morn: Double = 0.0
8 | )
9 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/source/RemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.source
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 |
5 | interface RemoteDataSource {
6 | suspend fun getForecast(lat: Double, lon: Double): Forecast
7 | }
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/Alerts.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model
2 |
3 | data class Alerts(
4 | val id: Long = 0,
5 | val from: Long = 0,
6 | val to: Long = 0,
7 | val isAlarm: Boolean = true,
8 | var workId: String = "",
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/enums/Location.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.enums
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.R
5 |
6 | enum class Location(@StringRes val stringRes: Int) {
7 | GPS(R.string.gps),
8 | MAP(R.string.map)
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/alerts/AlertsUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.alerts
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 |
5 | data class AlertsUiState(
6 | val alerts: List = emptyList(),
7 | val accent: Int = 0
8 | )
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/util/Response.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.util
2 |
3 | sealed class Response {
4 | data class Success(val result: T) : Response()
5 | data class Error(val throwable: Throwable, val result: T?) : Response()
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | gradle.properties
17 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/enums/Language.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.enums
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.R
5 |
6 | enum class Language(@StringRes val stringRes: Int) {
7 | ENGLISH(R.string.english),
8 | ARABIC(R.string.arabic)
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_maps.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/enums/WindSpeed.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.enums
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.R
5 |
6 | enum class WindSpeed(@StringRes val stringRes: Int) {
7 | METER_SECOND(R.string.meter_sec),
8 | MILE_HOUR(R.string.mile_hour)
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/enums/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.enums
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.R
5 |
6 | enum class Theme(@StringRes val stringRes: Int) {
7 | DEFAULT(R.string.default_),
8 | LIGHT(R.string.light),
9 | DARK(R.string.dark)
10 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/ForecastRequest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model
2 |
3 | import java.util.*
4 |
5 | data class ForecastRequest(
6 | val lat: Double = 0.0,
7 | val lon: Double = 0.0,
8 | val units: String = "metric",
9 | val lang: String = Locale.getDefault().language,
10 | )
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Temp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Temp(
4 | val day: Double = 0.0,
5 | val min: Double = 0.0,
6 | val max: Double = 0.0,
7 | val night: Double = 0.0,
8 | val eve: Double = 0.0,
9 | val morn: Double = 0.0
10 | )
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Alert.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Alert(
4 | val senderName: String = "",
5 | val event: String = "",
6 | val start: Int = 0,
7 | val end: Int = 0,
8 | val description: String = "",
9 | val tags: List = listOf()
10 | )
11 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "Weather"
10 | include ':app'
11 | include ':data'
12 | include ':domain'
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/dispatchers/IDispatchers.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.dispatchers
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | interface IDispatchers {
6 | val default: CoroutineDispatcher
7 | val main: CoroutineDispatcher
8 | val io: CoroutineDispatcher
9 | val unconfined: CoroutineDispatcher
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/enums/Temperature.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.enums
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.R
5 |
6 | enum class Temperature(@StringRes val stringRes: Int) {
7 | Celsius(R.string.celsius),
8 | Kelvin(R.string.kelvin),
9 | Fahrenheit(R.string.fahrenheit)
10 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/Settings.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model
2 |
3 | data class Settings(
4 | val location: Int,
5 | val language: Int,
6 | val theme: Int,
7 | val accent: Int,
8 | val notifications: Boolean,
9 | val temperature: Int,
10 | val windSpeed: Int,
11 | val versionCode: Int
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/map/MapUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.map
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import kotlinx.coroutines.Job
5 |
6 | data class MapUiState(
7 | val isLoading: Job? = null,
8 | val throwable: Throwable? = null,
9 | val forecast: Forecast = Forecast(),
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/home/HomeIntent.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.home
2 |
3 | sealed class HomeIntent {
4 | object Idle : HomeIntent()
5 | object GetMapForecast : HomeIntent()
6 | object RestorePreferences : HomeIntent()
7 | object ClearThrowable : HomeIntent()
8 | data class GetLocationForecast(val lat: Double, val lon: Double) : HomeIntent()
9 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/ForecastRequest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import java.util.*
5 |
6 | @Serializable
7 | data class ForecastRequest(
8 | val lat: Double = 0.0,
9 | val lon: Double = 0.0,
10 | val units: String = "metric",
11 | val lang: String = Locale.getDefault().language,
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorites/FavoritesUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorites
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 |
6 | data class FavoritesUiState(
7 | val favorites: List = emptyList(),
8 | val settings: Settings? = null,
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/dispatchers/IDispatchersImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util.dispatchers
2 |
3 | import kotlinx.coroutines.Dispatchers
4 |
5 | class IDispatchersImpl : IDispatchers {
6 | override val default = Dispatchers.Default
7 | override val main = Dispatchers.Main
8 | override val io = Dispatchers.IO
9 | override val unconfined = Dispatchers.Unconfined
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorite_details/FavoriteDetailsIntent.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorite_details
2 |
3 | sealed class FavoriteDetailsIntent {
4 | object Idle : FavoriteDetailsIntent()
5 | object RestorePreferences : FavoriteDetailsIntent()
6 | object ClearThrowable : FavoriteDetailsIntent()
7 | data class GetForecast(val id: Long) : FavoriteDetailsIntent()
8 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/alert/InsertAlert.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
5 |
6 | class InsertAlert(private val alertRepo: AlertRepo) {
7 | suspend fun execute(alert: Alerts) {
8 | alertRepo.insert(alert)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(5.dp),
9 | medium = RoundedCornerShape(10.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/alert/GetAlert.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
5 |
6 | class GetAlert(private val alertRepo: AlertRepo) {
7 | suspend fun execute(uuid: String): Alerts {
8 | return alertRepo.getAlert(uuid)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/home/HomeUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.home
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 |
6 | data class HomeUiState(
7 | val isLoading: Boolean = false,
8 | val throwable: Throwable? = null,
9 | val forecast: Forecast? = null,
10 | val settings: Settings? = null,
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/ThrowableMessage.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import com.github.amrmsaraya.weather.R
4 | import io.ktor.client.features.*
5 | import java.io.IOException
6 |
7 | fun Throwable.toStringResource(): Int? {
8 | return when (this) {
9 | is IOException -> R.string.check_your_connection_and_try_again
10 | is ResponseException -> R.string.you_have_exceeded_limit
11 | else -> null
12 | }
13 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/alert/GetAlerts.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | class GetAlerts(private val alertRepo: AlertRepo) {
8 | suspend fun execute(): Flow> {
9 | return alertRepo.getAlerts()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/InsertForecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 |
6 | class InsertForecast(private val forecastRepo: ForecastRepo) {
7 | suspend fun execute(forecast: Forecast) {
8 | forecastRepo.insertForecast(forecast)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/GetIntPreference.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | class GetIntPreference(private val preferencesRepo: PreferencesRepo) {
7 | suspend fun execute(key: String): Flow {
8 | return preferencesRepo.getIntPreference(key)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorites/FavoritesIntent.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorites
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 |
5 | sealed class FavoritesIntent {
6 | object Idle : FavoritesIntent()
7 | object RestorePreferences : FavoritesIntent()
8 | object GetFavoritesForecast : FavoritesIntent()
9 | data class DeleteForecasts(val favorites: List) : FavoritesIntent()
10 | }
--------------------------------------------------------------------------------
/data/src/test/java/com/github/amrmsaraya/weather/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/GetStringPreference.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | class GetStringPreference(private val preferencesRepo: PreferencesRepo) {
7 | suspend fun execute(key: String): Flow {
8 | return preferencesRepo.getStringPreference(key)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/alerts/AlertsIntent.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.alerts
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 |
5 | sealed class AlertsIntent {
6 | object Idle : AlertsIntent()
7 | object GetAccent : AlertsIntent()
8 | data class InsertAlert(val alert: Alerts) : AlertsIntent()
9 | data class DeleteAlerts(val alerts: List) : AlertsIntent()
10 | object GetAlerts : AlertsIntent()
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/GetBooleanPreference.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | class GetBooleanPreference(private val preferencesRepo: PreferencesRepo) {
7 | suspend fun execute(key: String): Flow {
8 | return preferencesRepo.getBooleanPreference(key)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/source/AlertDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.source
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface AlertDataSource {
7 | suspend fun insert(alert: Alerts)
8 | suspend fun delete(uuid: String)
9 | suspend fun delete(alerts: List)
10 | suspend fun getAlert(uuid: String): Alerts
11 | suspend fun getAlerts(): Flow>
12 | }
13 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Forecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Forecast(
4 | var id: Long = 0,
5 | val lat: Double = 0.0,
6 | val lon: Double = 0.0,
7 | val timezone: String = "",
8 | val timezoneOffset: Int = 0,
9 | val current: Current = Current(),
10 | val hourly: List = listOf(),
11 | val daily: List = listOf(),
12 | val alerts: List = listOf()
13 | )
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/repository/AlertRepo.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.repository
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface AlertRepo {
7 | suspend fun insert(alert: Alerts)
8 | suspend fun delete(uuid: String)
9 | suspend fun delete(alerts: List)
10 | suspend fun getAlert(uuid: String): Alerts
11 | suspend fun getAlerts(): Flow>
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorite_details/FavoriteDetailsUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorite_details
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 |
6 | data class FavoriteDetailsUiState(
7 | val isLoading: Boolean = false,
8 | val throwable: Throwable? = null,
9 | val forecast: Forecast? = null,
10 | val settings: Settings? = null,
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/map/MapIntent.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.map
2 |
3 | import androidx.annotation.StringRes
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 |
6 | sealed class MapIntent {
7 | object Idle : MapIntent()
8 | object GetCurrentForecast : MapIntent()
9 | data class GetForecastFromMap(val lat: Double, val lon: Double) : MapIntent()
10 | data class InsertForecast(val forecast: Forecast) : MapIntent()
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/SetDefaultPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
5 |
6 | class SetDefaultPreferences(private val preferencesRepo: PreferencesRepo) {
7 | suspend fun execute(settings: Settings) {
8 | preferencesRepo.setDefaultPreferences(settings)
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/FeelsLikeDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class FeelsLikeDTO(
9 | @SerialName("day")
10 | val day: Double = 0.0,
11 | @SerialName("night")
12 | val night: Double = 0.0,
13 | @SerialName("eve")
14 | val eve: Double = 0.0,
15 | @SerialName("morn")
16 | val morn: Double = 0.0
17 | )
18 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/WeatherDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class WeatherDTO(
9 | @SerialName("id")
10 | val id: Int = 0,
11 | @SerialName("main")
12 | val main: String = "",
13 | @SerialName("description")
14 | val description: String = "",
15 | @SerialName("icon")
16 | val icon: String = ""
17 | )
18 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/alert/DeleteAlert.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
5 |
6 | class DeleteAlert(private val alertRepo: AlertRepo) {
7 | suspend fun execute(uuid: String) {
8 | alertRepo.delete(uuid)
9 | }
10 |
11 | suspend fun execute(alerts: List) {
12 | alertRepo.delete(alerts)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/GetFavoriteForecasts.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | class GetFavoriteForecasts(private val forecastRepo: ForecastRepo) {
8 | suspend fun execute(): Flow> {
9 | return forecastRepo.getFavoriteForecasts()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/RestorePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | class RestorePreferences(private val preferencesRepo: PreferencesRepo) {
8 | suspend fun execute(): Flow {
9 | return preferencesRepo.restorePreferences()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloud.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF121212
4 | #FF9E9E9E
5 | #FF1976D2
6 | #FFFF00E4
7 | #FF4BC0C8
8 | #FFFFC371
9 | #FFFEAC5A
10 | #FFE29587
11 | #FFE7BFE8
12 |
13 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'kotlin'
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_8
8 | targetCompatibility = JavaVersion.VERSION_1_8
9 | }
10 |
11 | dependencies {
12 | implementation 'junit:junit:4.13.2'
13 | def coroutines_version = '1.5.2'
14 |
15 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
16 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
17 | testImplementation "com.google.truth:truth:1.1.3"
18 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/DeleteForecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 |
6 | class DeleteForecast(private val forecastRepo: ForecastRepo) {
7 | suspend fun execute(forecast: Forecast) {
8 | forecastRepo.deleteForecast(forecast)
9 | }
10 |
11 | suspend fun execute(list: List) {
12 | forecastRepo.deleteForecast(list)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/visibility.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/source/LocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.source
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface LocalDataSource {
7 | suspend fun insertForecast(forecast: Forecast)
8 | suspend fun deleteForecast(forecast: Forecast)
9 | suspend fun deleteForecast(list: List)
10 | suspend fun getForecast(id:Long): Forecast
11 | suspend fun getCurrentForecast(): Forecast
12 | suspend fun getFavoriteForecasts(): Flow>
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Current.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Current(
4 | val dt: Int = 0,
5 | val sunrise: Int = 0,
6 | val sunset: Int = 0,
7 | val temp: Double = 0.0,
8 | val feelsLike: Double = 0.0,
9 | val pressure: Int = 0,
10 | val humidity: Int = 0,
11 | val dewPoint: Double = 0.0,
12 | val uvi: Double = 0.0,
13 | val clouds: Int = 0,
14 | val visibility: Int = 0,
15 | val windSpeed: Double = 0.0,
16 | val windDeg: Int = 0,
17 | val weather: List = listOf()
18 | )
19 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/remote/ApiService.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.remote
2 |
3 | import com.github.amrmsaraya.weather.data.model.forecast.ForecastDTO
4 | import io.ktor.client.*
5 | import io.ktor.client.request.*
6 | import java.util.*
7 |
8 | class ApiService(private val client: HttpClient) {
9 |
10 | suspend fun getForecast(lat: Double, lon: Double): ForecastDTO = client.get("onecall") {
11 | parameter("lat", lat)
12 | parameter("lon", lon)
13 | parameter("units", "metric")
14 | parameter("lang", Locale.getDefault().language)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Hourly.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Hourly(
4 | val dt: Int = 0,
5 | val temp: Double = 0.0,
6 | val feelsLike: Double = 0.0,
7 | val pressure: Int = 0,
8 | val humidity: Int = 0,
9 | val dewPoint: Double = 0.0,
10 | val uvi: Double = 0.0,
11 | val clouds: Int = 0,
12 | val visibility: Int = 0,
13 | val windSpeed: Double = 0.0,
14 | val windDeg: Int = 0,
15 | val windGust: Double = 0.0,
16 | val weather: List = listOf(),
17 | val pop: Double = 0.0,
18 | )
19 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/TempDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class TempDTO(
9 | @SerialName("day")
10 | val day: Double = 0.0,
11 | @SerialName("min")
12 | val min: Double = 0.0,
13 | @SerialName("max")
14 | val max: Double = 0.0,
15 | @SerialName("night")
16 | val night: Double = 0.0,
17 | @SerialName("eve")
18 | val eve: Double = 0.0,
19 | @SerialName("morn")
20 | val morn: Double = 0.0
21 | )
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/Application.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import android.app.Application
4 | import androidx.hilt.work.HiltWorkerFactory
5 | import androidx.work.Configuration
6 | import dagger.hilt.android.HiltAndroidApp
7 | import javax.inject.Inject
8 |
9 | @HiltAndroidApp
10 | class Application : Application(), Configuration.Provider {
11 |
12 | @Inject
13 | lateinit var workerFactory: HiltWorkerFactory
14 |
15 | override fun getWorkManagerConfiguration() =
16 | Configuration.Builder()
17 | .setWorkerFactory(workerFactory)
18 | .build()
19 |
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/Dispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
4 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchersImpl
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class Dispatcher {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideCoroutineDispatcher(): IDispatchers {
18 | return IDispatchersImpl()
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Primary = Color(0xFF1976D2)
6 | val Secondary = Color(0xFFFF00E4)
7 |
8 | val Primary1 = Color(0xFFFF6B6B)
9 | val Secondary1 = Color(0xFF4BC0C8)
10 |
11 | val Primary2 = Color(0xFFFF5F6D)
12 | val Secondary2 = Color(0xFFFFC371)
13 |
14 | val Primary3 = Color(0xFF4BC0C8)
15 | val Secondary3 = Color(0xFFFEAC5A)
16 |
17 | val Primary4 = Color(0xFFD66D75)
18 | val Secondary4 = Color(0xFFE29587)
19 |
20 | val Primary5 = Color(0xFF6190E8)
21 | val Secondary5 = Color(0xFFE7BFE8)
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/AlertsDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "alerts")
8 | data class AlertsDTO(
9 | @PrimaryKey(autoGenerate = true)
10 | @ColumnInfo(name = "id")
11 | val id: Long = 0,
12 | @ColumnInfo(name = "start")
13 | val from: Long = 0,
14 | @ColumnInfo(name = "end")
15 | val to: Long = 0,
16 | @ColumnInfo(name = "type")
17 | val isAlarm: Boolean = true,
18 | @ColumnInfo(name = "work_id")
19 | var workId: String = "",
20 | )
21 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/sourceImp/RemoteDataSourceImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.sourceImp
2 |
3 | import com.github.amrmsaraya.weather.data.mapper.toForecast
4 | import com.github.amrmsaraya.weather.data.remote.ApiService
5 | import com.github.amrmsaraya.weather.data.source.RemoteDataSource
6 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
7 |
8 | class RemoteDataSourceImp(private val apiService: ApiService) : RemoteDataSource {
9 |
10 | override suspend fun getForecast(lat: Double, lon: Double): Forecast {
11 | return apiService.getForecast(lat, lon).toForecast()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/GetForecastFromMap.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 |
6 | class GetForecastFromMap(private val forecastRepo: ForecastRepo) {
7 | suspend fun execute(lat: Double, lon: Double) {
8 | runCatching { forecastRepo.getRemoteForecast(lat, lon) }
9 | .onSuccess { forecastRepo.insertForecast(it) }
10 | .onFailure { forecastRepo.insertForecast(Forecast(lat = lat, lon = lon)) }
11 | }
12 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/mapper/AlertsMapper.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.mapper
2 |
3 | import com.github.amrmsaraya.weather.data.model.AlertsDTO
4 | import com.github.amrmsaraya.weather.domain.model.Alerts
5 |
6 | fun AlertsDTO.toAlerts(): Alerts {
7 | return Alerts(
8 | id = id,
9 | from = from,
10 | to = to,
11 | isAlarm = isAlarm,
12 | workId = workId
13 | )
14 | }
15 |
16 | fun Alerts.toDTO(): AlertsDTO {
17 | return AlertsDTO(
18 | id = id,
19 | from = from,
20 | to = to,
21 | isAlarm = isAlarm,
22 | workId = workId
23 | )
24 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/AlertDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class AlertDTO(
9 | @SerialName("sender_name")
10 | val senderName: String = "",
11 | @SerialName("event")
12 | val event: String = "",
13 | @SerialName("start")
14 | val start: Int = 0,
15 | @SerialName("end")
16 | val end: Int = 0,
17 | @SerialName("description")
18 | val description: String = "",
19 | @SerialName("tags")
20 | val tags: List = listOf()
21 | )
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/ForecastIcons.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import com.github.amrmsaraya.weather.R
6 |
7 | enum class ForecastIcons(@StringRes val nameId: Int, @DrawableRes val icon: Int) {
8 | Pressure(R.string.pressure, R.drawable.speedometer),
9 | Cloud(R.string.cloud, R.drawable.cloud),
10 | Wind(R.string.wind, R.drawable.wind_speed),
11 | UltraViolet(R.string.ultra_violet, R.drawable.ultraviolet),
12 | Humidity(R.string.humidity, R.drawable.humidity),
13 | Visibility(R.string.visibility, R.drawable.visibility),
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/preferences/SavePreference.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.preferences
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
4 |
5 | class SavePreference(private val preferencesRepo: PreferencesRepo) {
6 | suspend fun execute(key: String, value: Int) {
7 | preferencesRepo.savePreference(key, value)
8 | }
9 |
10 | suspend fun execute(key: String, value: Boolean) {
11 | preferencesRepo.savePreference(key, value)
12 | }
13 |
14 | suspend fun execute(key: String, value: String) {
15 | preferencesRepo.savePreference(key, value)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/wind_speed.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/UpdateFavoritesForecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
4 | import kotlinx.coroutines.flow.first
5 |
6 |
7 | class UpdateFavoritesForecast constructor(private val forecastRepo: ForecastRepo) {
8 | suspend fun execute() {
9 | val favorites = forecastRepo.getFavoriteForecasts().first()
10 | favorites.forEach {
11 | runCatching {
12 | val updatedForecast = forecastRepo.getRemoteForecast(it.lat, it.lon)
13 | forecastRepo.insertForecast(updatedForecast.copy(id = it.id))
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/model/forecast/Daily.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.model.forecast
2 |
3 | data class Daily(
4 | val dt: Int = 0,
5 | val sunrise: Int = 0,
6 | val sunset: Int = 0,
7 | val moonrise: Int = 0,
8 | val moonset: Int = 0,
9 | val moonPhase: Double = 0.0,
10 | val temp: Temp,
11 | val feelsLike: FeelsLike = FeelsLike(),
12 | val pressure: Int = 0,
13 | val humidity: Int = 0,
14 | val dewPoint: Double = 0.0,
15 | val windSpeed: Double = 0.0,
16 | val windDeg: Int = 0,
17 | val windGust: Double = 0.0,
18 | val weather: List = listOf(),
19 | val clouds: Int = 0,
20 | val pop: Double = 0.0,
21 | val uvi: Double = 0.0
22 | )
23 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/repository/PreferencesRepo.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.repository
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Settings
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface PreferencesRepo {
7 | suspend fun savePreference(key: String, value: Int)
8 | suspend fun savePreference(key: String, value: Boolean)
9 | suspend fun savePreference(key: String, value: String)
10 | suspend fun getIntPreference(key: String): Flow
11 | suspend fun getBooleanPreference(key: String): Flow
12 | suspend fun getStringPreference(key: String): Flow
13 | suspend fun setDefaultPreferences(settings: Settings)
14 | suspend fun restorePreferences(): Flow
15 | }
16 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/local/AlertDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.local
2 |
3 | import androidx.room.*
4 | import com.github.amrmsaraya.weather.data.model.AlertsDTO
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | @Dao
8 | interface AlertDao {
9 | @Insert(onConflict = OnConflictStrategy.REPLACE)
10 | suspend fun insert(alert: AlertsDTO)
11 |
12 | @Delete
13 | suspend fun delete(alerts: List)
14 |
15 | @Query("DELETE FROM alerts WHERE work_id = :uuid")
16 | suspend fun delete(uuid: String)
17 |
18 | @Query("SELECT * FROM alerts WHERE work_id = :uuid")
19 | suspend fun getAlert(uuid: String): AlertsDTO
20 |
21 | @Query("SELECT * FROM alerts")
22 | fun getAlerts(): Flow>
23 | }
24 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/repository/ForecastRepo.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.repository
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface ForecastRepo {
7 | suspend fun insertForecast(forecast: Forecast)
8 | suspend fun deleteForecast(forecast: Forecast)
9 | suspend fun deleteForecast(list: List)
10 | suspend fun getLocalForecast(id: Long): Forecast
11 | suspend fun getRemoteForecast(lat: Double, lon: Double): Forecast
12 | suspend fun getCurrentForecast(lat: Double, lon: Double, forceUpdate: Boolean): Forecast
13 | suspend fun getCurrentForecast(): Forecast
14 | suspend fun getFavoriteForecasts(): Flow>
15 | }
16 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/LocaleHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import android.content.Context
4 | import android.content.ContextWrapper
5 | import android.os.LocaleList
6 | import java.util.*
7 |
8 | object LocaleHelper {
9 | fun setLocale(mContext: Context, newLocale: Locale): ContextWrapper {
10 | var context = mContext
11 | val res = context.applicationContext.resources
12 | val configuration = res.configuration
13 | configuration.setLocale(newLocale)
14 | configuration.setLayoutDirection(newLocale)
15 | val localeList = LocaleList(newLocale)
16 | LocaleList.setDefault(localeList)
17 | configuration.setLocales(localeList)
18 | context = context.createConfigurationContext(configuration)
19 | return ContextWrapper(context)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloudy_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/DataStore.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.preferencesDataStore
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 |
14 | private val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | class DataStore {
19 | @Singleton
20 | @Provides
21 | fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore {
22 | return context.dataStore
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/repository/FakeAlertRepo.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.repository
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flow
6 |
7 | class FakeAlertRepo : AlertRepo {
8 | val alertsList = mutableListOf()
9 |
10 | override suspend fun insert(alert: Alerts) {
11 | alertsList.add(alert)
12 | }
13 |
14 | override suspend fun delete(uuid: String) {
15 | alertsList.removeIf { it.workId == uuid }
16 | }
17 |
18 | override suspend fun delete(alerts: List) {
19 | alertsList.removeAll(alerts)
20 | }
21 |
22 | override suspend fun getAlert(uuid: String): Alerts {
23 | return alertsList.first { it.workId == uuid }
24 | }
25 |
26 | override suspend fun getAlerts(): Flow> {
27 | return flow {
28 | emit(alertsList)
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/local/WeatherDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.local
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy.REPLACE
7 | import androidx.room.Query
8 | import com.github.amrmsaraya.weather.data.model.forecast.ForecastDTO
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface WeatherDao {
13 | @Insert(onConflict = REPLACE)
14 | suspend fun insertForecast(forecast: ForecastDTO)
15 |
16 | @Delete
17 | suspend fun deleteForecast(forecast: ForecastDTO)
18 |
19 | @Delete
20 | suspend fun deleteForecast(list: List)
21 |
22 | @Query("SELECT * FROM forecast WHERE id = :id")
23 | fun getForecast(id: Long): ForecastDTO
24 |
25 | @Query("SELECT * FROM forecast WHERE id = 1")
26 | fun getCurrentForecast(): ForecastDTO
27 |
28 | @Query("SELECT * FROM forecast WHERE id != 1")
29 | fun getFavoriteForecasts(): Flow>
30 | }
31 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/repositoryImp/AlertRepoImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.repositoryImp
2 |
3 | import com.github.amrmsaraya.weather.data.source.AlertDataSource
4 | import com.github.amrmsaraya.weather.domain.model.Alerts
5 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | class AlertRepoImp(private val alertDataSource: AlertDataSource) : AlertRepo {
9 | override suspend fun insert(alert: Alerts) {
10 | alertDataSource.insert(alert)
11 | }
12 |
13 | override suspend fun delete(uuid: String) {
14 | alertDataSource.delete(uuid)
15 | }
16 |
17 | override suspend fun delete(alerts: List) {
18 | alertDataSource.delete(alerts)
19 | }
20 |
21 | override suspend fun getAlert(uuid: String): Alerts {
22 | return alertDataSource.getAlert(uuid)
23 | }
24 |
25 | override suspend fun getAlerts(): Flow> {
26 | return alertDataSource.getAlerts()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/GetForecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 | import com.github.amrmsaraya.weather.domain.util.Response
6 |
7 | class GetForecast(private val forecastRepo: ForecastRepo) {
8 | suspend fun execute(id: Long): Response {
9 | return runCatching {
10 | val cached = forecastRepo.getLocalForecast(id)
11 | val response = forecastRepo.getRemoteForecast(cached.lat, cached.lon)
12 | forecastRepo.insertForecast(response.copy(id = id))
13 | Response.Success(forecastRepo.getLocalForecast(id))
14 | }.getOrElse {
15 | runCatching {
16 | val response = forecastRepo.getLocalForecast(id)
17 | Response.Error(it, response)
18 | }.getOrElse {
19 | Response.Error(it, null)
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/humidity.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/Room.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import android.content.Context
4 | import com.github.amrmsaraya.weather.data.local.AlertDao
5 | import com.github.amrmsaraya.weather.data.local.WeatherDao
6 | import com.github.amrmsaraya.weather.data.local.WeatherDatabase
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import kotlinx.serialization.ExperimentalSerializationApi
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | @ExperimentalSerializationApi
18 | class Room {
19 |
20 | @Singleton
21 | @Provides
22 | fun provideWeatherDao(@ApplicationContext context: Context): WeatherDao {
23 | return WeatherDatabase.getDatabase(context).weatherDao()
24 | }
25 |
26 | @Singleton
27 | @Provides
28 | fun provideAlertDao(@ApplicationContext context: Context): AlertDao {
29 | return WeatherDatabase.getDatabase(context).alertDao()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/alert/InsertAlertTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.FakeAlertRepo
5 | import com.google.common.truth.Truth
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | @ExperimentalCoroutinesApi
12 | class InsertAlertTest {
13 | private lateinit var fakeAlertRepo: FakeAlertRepo
14 | private lateinit var insertAlert: InsertAlert
15 |
16 | @Before
17 | fun setup() {
18 | fakeAlertRepo = FakeAlertRepo()
19 | insertAlert = InsertAlert(fakeAlertRepo)
20 | }
21 |
22 | @Test
23 | fun `execute() insert the given alert`() = runBlockingTest {
24 | // Given
25 | val alert = Alerts(workId = "12")
26 |
27 | // When
28 | insertAlert.execute(alert)
29 |
30 | // Then
31 | Truth.assertThat(fakeAlertRepo.alertsList).contains(alert)
32 | }
33 | }
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/alert/GetAlertTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.FakeAlertRepo
5 | import com.google.common.truth.Truth
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | @ExperimentalCoroutinesApi
12 | class GetAlertTest {
13 | private lateinit var fakeAlertRepo: FakeAlertRepo
14 | private lateinit var getAlert: GetAlert
15 |
16 | @Before
17 | fun setup() {
18 | fakeAlertRepo = FakeAlertRepo()
19 | getAlert = GetAlert(fakeAlertRepo)
20 | }
21 |
22 | @Test
23 | fun `execute() with uuid then return alert`() = runBlockingTest {
24 | // Given
25 | val alert = Alerts(workId = "1")
26 | fakeAlertRepo.insert(alert)
27 |
28 | // when
29 | val result = getAlert.execute("1")
30 |
31 | // Then
32 | Truth.assertThat(result).isEqualTo(alert)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
12 |
18 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/CurrentDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class CurrentDTO(
9 | @SerialName("dt")
10 | val dt: Int = 0,
11 | @SerialName("sunrise")
12 | val sunrise: Int = 0,
13 | @SerialName("sunset")
14 | val sunset: Int = 0,
15 | @SerialName("temp")
16 | val temp: Double = 0.0,
17 | @SerialName("feels_like")
18 | val feelsLike: Double = 0.0,
19 | @SerialName("pressure")
20 | val pressure: Int = 0,
21 | @SerialName("humidity")
22 | val humidity: Int = 0,
23 | @SerialName("dew_point")
24 | val dewPoint: Double = 0.0,
25 | @SerialName("uvi")
26 | val uvi: Double = 0.0,
27 | @SerialName("clouds")
28 | val clouds: Int = 0,
29 | @SerialName("visibility")
30 | val visibility: Int = 0,
31 | @SerialName("wind_speed")
32 | val windSpeed: Double = 0.0,
33 | @SerialName("wind_deg")
34 | val windDeg: Int = 0,
35 | @SerialName("weather")
36 | val weather: List = listOf()
37 | )
38 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/HourlyDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class HourlyDTO(
9 | @SerialName("dt")
10 | val dt: Int = 0,
11 | @SerialName("temp")
12 | val temp: Double = 0.0,
13 | @SerialName("feels_like")
14 | val feelsLike: Double = 0.0,
15 | @SerialName("pressure")
16 | val pressure: Int = 0,
17 | @SerialName("humidity")
18 | val humidity: Int = 0,
19 | @SerialName("dew_point")
20 | val dewPoint: Double = 0.0,
21 | @SerialName("uvi")
22 | val uvi: Double = 0.0,
23 | @SerialName("clouds")
24 | val clouds: Int = 0,
25 | @SerialName("visibility")
26 | val visibility: Int = 0,
27 | @SerialName("wind_speed")
28 | val windSpeed: Double = 0.0,
29 | @SerialName("wind_deg")
30 | val windDeg: Int = 0,
31 | @SerialName("wind_gust")
32 | val windGust: Double = 0.0,
33 | @SerialName("weather")
34 | val weather: List = listOf(),
35 | @SerialName("pop")
36 | val pop: Double = 0.0,
37 | )
38 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/sourceImp/AlertDataSourceImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.sourceImp
2 |
3 | import com.github.amrmsaraya.weather.data.local.AlertDao
4 | import com.github.amrmsaraya.weather.data.mapper.toAlerts
5 | import com.github.amrmsaraya.weather.data.mapper.toDTO
6 | import com.github.amrmsaraya.weather.data.source.AlertDataSource
7 | import com.github.amrmsaraya.weather.domain.model.Alerts
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.map
10 |
11 | class AlertDataSourceImp(private val alertDao: AlertDao) : AlertDataSource {
12 | override suspend fun insert(alert: Alerts) {
13 | alertDao.insert(alert.toDTO())
14 | }
15 |
16 | override suspend fun delete(uuid: String) {
17 | alertDao.delete(uuid)
18 | }
19 |
20 | override suspend fun delete(alerts: List) {
21 | alertDao.delete(alerts.map { it.toDTO() })
22 | }
23 |
24 | override suspend fun getAlert(uuid: String): Alerts {
25 | return alertDao.getAlert(uuid).toAlerts()
26 | }
27 |
28 | override suspend fun getAlerts(): Flow> {
29 | return alertDao.getAlerts().map { list -> list.map { it.toAlerts() } }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/alert/DeleteAlertTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.alert
2 |
3 | import com.github.amrmsaraya.weather.domain.model.Alerts
4 | import com.github.amrmsaraya.weather.domain.repository.FakeAlertRepo
5 | import com.google.common.truth.Truth
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | @ExperimentalCoroutinesApi
12 | class DeleteAlertTest {
13 | private lateinit var fakeAlertRepo: FakeAlertRepo
14 | private lateinit var deleteAlert: DeleteAlert
15 |
16 | @Before
17 | fun setup() {
18 | fakeAlertRepo = FakeAlertRepo()
19 | deleteAlert = DeleteAlert(fakeAlertRepo)
20 | }
21 |
22 | @Test
23 | fun `execute() with alert we want to delete then it should be deleted`() = runBlockingTest {
24 | // Given
25 | val alert = Alerts(workId = "1234")
26 | val alert2 = Alerts(workId = "2468")
27 | fakeAlertRepo.insert(alert)
28 | fakeAlertRepo.insert(alert2)
29 |
30 | // When
31 | deleteAlert.execute("2468")
32 |
33 | // Then
34 | Truth.assertThat(fakeAlertRepo.alertsList).isEqualTo(listOf(alert))
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/speedometer.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/usecase/AlertModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di.usecase
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
4 | import com.github.amrmsaraya.weather.domain.usecase.alert.DeleteAlert
5 | import com.github.amrmsaraya.weather.domain.usecase.alert.GetAlert
6 | import com.github.amrmsaraya.weather.domain.usecase.alert.GetAlerts
7 | import com.github.amrmsaraya.weather.domain.usecase.alert.InsertAlert
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | class AlertModule {
17 |
18 | @Singleton
19 | @Provides
20 | fun provideDeleteAlert(alertRepo: AlertRepo): DeleteAlert {
21 | return DeleteAlert(alertRepo)
22 | }
23 |
24 | @Singleton
25 | @Provides
26 | fun provideDGetAlert(alertRepo: AlertRepo): GetAlert {
27 | return GetAlert(alertRepo)
28 | }
29 |
30 | @Singleton
31 | @Provides
32 | fun provideGetAlerts(alertRepo: AlertRepo): GetAlerts {
33 | return GetAlerts(alertRepo)
34 | }
35 |
36 | @Singleton
37 | @Provides
38 | fun provideInsertAlert(alertRepo: AlertRepo): InsertAlert {
39 | return InsertAlert(alertRepo)
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import com.github.amrmsaraya.weather.R
10 |
11 | val Spartan = FontFamily(
12 | Font(R.font.spartan_bold)
13 | )
14 |
15 | val Cairo = FontFamily(
16 | Font(R.font.cairo_regular, FontWeight.Normal),
17 | Font(R.font.cairo_bold, FontWeight.Bold)
18 | )
19 |
20 |
21 | // Set of Material typography styles to start with
22 | val Typography = Typography(
23 | body1 = TextStyle(
24 | fontFamily = FontFamily.Default,
25 | fontWeight = FontWeight.Normal,
26 | fontSize = 16.sp
27 | ),
28 | /* Other default text styles to override
29 | button = TextStyle(
30 | fontFamily = FontFamily.Default,
31 | fontWeight = FontWeight.W500,
32 | fontSize = 14.sp
33 | ),
34 | caption = TextStyle(
35 | fontFamily = FontFamily.Default,
36 | fontWeight = FontWeight.Normal,
37 | fontSize = 12.sp
38 | )
39 | */
40 | )
41 |
42 | val ArabicTypography = Typography(defaultFontFamily = Cairo)
43 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/GetForecastFromMapTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Current
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
6 | import com.google.common.truth.Truth
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.test.runBlockingTest
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | @ExperimentalCoroutinesApi
13 | class GetForecastFromMapTest {
14 | private lateinit var fakeForecastRepo: FakeForecastRepo
15 | private lateinit var getForecastFromMap: GetForecastFromMap
16 |
17 | @Before
18 | fun setup() {
19 | fakeForecastRepo = FakeForecastRepo()
20 | getForecastFromMap = GetForecastFromMap(fakeForecastRepo)
21 | }
22 |
23 | @Test
24 | fun `execute() with lat & lon then insert the new forecast to forecastList`() =
25 | runBlockingTest {
26 | // When
27 | getForecastFromMap.execute(20.0, 30.0)
28 |
29 | // Then
30 | Truth.assertThat(Forecast(lat = 20.0, lon = 30.0, current = Current(temp = 28.6))).isIn(
31 | fakeForecastRepo.forecastList
32 | )
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/storm.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/storm_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/ForecastDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 |
9 | @Entity(tableName = "forecast")
10 | @Serializable
11 | data class ForecastDTO(
12 | @PrimaryKey(autoGenerate = true)
13 | @ColumnInfo(name = "id")
14 | var id: Long = 0,
15 |
16 | @SerialName("lat")
17 | @ColumnInfo(name = "lat")
18 | val lat: Double = 0.0,
19 |
20 | @SerialName("lon")
21 | @ColumnInfo(name = "lon")
22 | val lon: Double = 0.0,
23 |
24 | @SerialName("timezone")
25 | @ColumnInfo(name = "timezone")
26 | val timezone: String = "",
27 |
28 | @SerialName("timezone_offset")
29 | @ColumnInfo(name = "timezone_offset")
30 | val timezoneOffset: Int = 0,
31 |
32 | @SerialName("current")
33 | @ColumnInfo(name = "current")
34 | val current: CurrentDTO = CurrentDTO(),
35 |
36 | @SerialName("hourly")
37 | @ColumnInfo(name = "hourly")
38 | val hourly: List = listOf(),
39 |
40 | @SerialName("daily")
41 | @ColumnInfo(name = "daily")
42 | val daily: List = listOf(),
43 |
44 | @SerialName("alerts")
45 | @ColumnInfo(name = "alerts")
46 | val alerts: List = listOf()
47 | )
48 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rainy_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/model/forecast/DailyDTO.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.model.forecast
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class DailyDTO(
9 | @SerialName("dt")
10 | val dt: Int = 0,
11 | @SerialName("sunrise")
12 | val sunrise: Int = 0,
13 | @SerialName("sunset")
14 | val sunset: Int = 0,
15 | @SerialName("moonrise")
16 | val moonrise: Int = 0,
17 | @SerialName("moonset")
18 | val moonset: Int = 0,
19 | @SerialName("moon_phase")
20 | val moonPhase: Double = 0.0,
21 | @SerialName("temp")
22 | val temp: TempDTO,
23 | @SerialName("feels_like")
24 | val feelsLike: FeelsLikeDTO = FeelsLikeDTO(),
25 | @SerialName("pressure")
26 | val pressure: Int = 0,
27 | @SerialName("humidity")
28 | val humidity: Int = 0,
29 | @SerialName("dew_point")
30 | val dewPoint: Double = 0.0,
31 | @SerialName("wind_speed")
32 | val windSpeed: Double = 0.0,
33 | @SerialName("wind_deg")
34 | val windDeg: Int = 0,
35 | @SerialName("wind_gust")
36 | val windGust: Double = 0.0,
37 | @SerialName("weather")
38 | val weather: List = listOf(),
39 | @SerialName("clouds")
40 | val clouds: Int = 0,
41 | @SerialName("pop")
42 | val pop: Double = 0.0,
43 | @SerialName("uvi")
44 | val uvi: Double = 0.0
45 | )
46 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/sourceImp/LocalDataSourceImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.sourceImp
2 |
3 | import com.github.amrmsaraya.weather.data.local.WeatherDao
4 | import com.github.amrmsaraya.weather.data.mapper.toDTO
5 | import com.github.amrmsaraya.weather.data.mapper.toForecast
6 | import com.github.amrmsaraya.weather.data.source.LocalDataSource
7 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.map
10 |
11 | class LocalDataSourceImp(private val weatherDao: WeatherDao) : LocalDataSource {
12 | override suspend fun insertForecast(forecast: Forecast) {
13 | weatherDao.insertForecast(forecast.toDTO())
14 | }
15 |
16 | override suspend fun deleteForecast(forecast: Forecast) {
17 | weatherDao.deleteForecast(forecast.toDTO())
18 | }
19 |
20 | override suspend fun deleteForecast(list: List) {
21 | weatherDao.deleteForecast(list.map { it.toDTO() })
22 | }
23 |
24 | override suspend fun getForecast(id:Long): Forecast {
25 | return weatherDao.getForecast(id).toForecast()
26 | }
27 |
28 | override suspend fun getCurrentForecast(): Forecast {
29 | return weatherDao.getCurrentForecast().toForecast()
30 | }
31 |
32 | override suspend fun getFavoriteForecasts(): Flow> {
33 | return weatherDao.getFavoriteForecasts().map { list -> list.map { it.toForecast() } }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/DataSource.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import com.github.amrmsaraya.weather.data.local.AlertDao
4 | import com.github.amrmsaraya.weather.data.local.WeatherDao
5 | import com.github.amrmsaraya.weather.data.remote.ApiService
6 | import com.github.amrmsaraya.weather.data.source.AlertDataSource
7 | import com.github.amrmsaraya.weather.data.source.LocalDataSource
8 | import com.github.amrmsaraya.weather.data.source.RemoteDataSource
9 | import com.github.amrmsaraya.weather.data.sourceImp.AlertDataSourceImp
10 | import com.github.amrmsaraya.weather.data.sourceImp.LocalDataSourceImp
11 | import com.github.amrmsaraya.weather.data.sourceImp.RemoteDataSourceImp
12 | import dagger.Module
13 | import dagger.Provides
14 | import dagger.hilt.InstallIn
15 | import dagger.hilt.components.SingletonComponent
16 | import javax.inject.Singleton
17 |
18 |
19 | @Module
20 | @InstallIn(SingletonComponent::class)
21 | class DataSource {
22 |
23 | @Provides
24 | @Singleton
25 | fun provideLocalDataSource(weatherDao: WeatherDao): LocalDataSource {
26 | return LocalDataSourceImp(weatherDao)
27 | }
28 |
29 | @Provides
30 | @Singleton
31 | fun provideRemoteDataSource(apiService: ApiService): RemoteDataSource {
32 | return RemoteDataSourceImp(apiService)
33 | }
34 |
35 | @Provides
36 | @Singleton
37 | fun provideAlertLocalDataSource(alertDao: AlertDao): AlertDataSource {
38 | return AlertDataSourceImp(alertDao)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/local/WeatherDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.local
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import androidx.room.TypeConverters
8 | import com.github.amrmsaraya.weather.data.model.AlertsDTO
9 | import com.github.amrmsaraya.weather.data.model.forecast.ForecastDTO
10 | import com.github.amrmsaraya.weather.data.util.Converter
11 | import kotlinx.serialization.ExperimentalSerializationApi
12 |
13 | @ExperimentalSerializationApi
14 | @Database(entities = [ForecastDTO::class, AlertsDTO::class], version = 2)
15 | @TypeConverters(Converter::class)
16 | abstract class WeatherDatabase : RoomDatabase() {
17 |
18 | abstract fun weatherDao(): WeatherDao
19 | abstract fun alertDao(): AlertDao
20 |
21 | companion object {
22 | @Volatile
23 | private var INSTANCE: WeatherDatabase? = null
24 |
25 | fun getDatabase(context: Context): WeatherDatabase {
26 | return INSTANCE ?: synchronized(this) {
27 | val instance = Room.databaseBuilder(
28 | context.applicationContext,
29 | WeatherDatabase::class.java,
30 | "weather_database"
31 | )
32 | .fallbackToDestructiveMigration()
33 | .build()
34 | INSTANCE = instance
35 |
36 | instance
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/NotificationHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import android.os.Build
8 | import androidx.annotation.RequiresApi
9 | import androidx.core.app.NotificationCompat
10 | import com.github.amrmsaraya.weather.R
11 |
12 |
13 | class NotificationHelper(private val context: Context) {
14 |
15 | private val channelId = context.packageName
16 | private val channelName = "Weather"
17 | val manager: NotificationManager =
18 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
19 |
20 | init {
21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
22 | createChannel()
23 | }
24 | }
25 |
26 | @RequiresApi(Build.VERSION_CODES.O)
27 | private fun createChannel() {
28 | val channel =
29 | NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
30 | manager.createNotificationChannel(channel)
31 | }
32 |
33 | fun getNotification(event: String, description: String): Notification {
34 | return NotificationCompat.Builder(context.applicationContext, channelId).apply {
35 | setContentTitle(event)
36 | setContentText(description)
37 | setAutoCancel(true)
38 | setSilent(true)
39 | setSmallIcon(R.drawable.cloud)
40 | }.build()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/GetFavoriteForecastsTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
5 | import com.google.common.truth.Truth.assertThat
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.test.runBlockingTest
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | @ExperimentalCoroutinesApi
13 | class GetFavoriteForecastsTest {
14 | private lateinit var fakeForecastRepo: FakeForecastRepo
15 | private lateinit var getFavoriteForecasts: GetFavoriteForecasts
16 |
17 | @Before
18 | fun setup() {
19 | fakeForecastRepo = FakeForecastRepo()
20 | getFavoriteForecasts = GetFavoriteForecasts(fakeForecastRepo)
21 | }
22 |
23 | @Test
24 | fun `getFavoriteForecasts() then return flow contain list of favorite forecasts`() =
25 | runBlockingTest {
26 | // Given
27 | val forecasts = listOf(
28 | Forecast(id = 1),
29 | Forecast(id = 2),
30 | Forecast(id = 3),
31 | Forecast(id = 4),
32 | )
33 | forecasts.forEach { fakeForecastRepo.insertForecast(it) }
34 |
35 | // When
36 | val favorites = getFavoriteForecasts.execute().first()
37 |
38 | // Then
39 | assertThat(favorites).isEqualTo(forecasts.filter { it.id != 1L })
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/usecase/PreferenceModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di.usecase
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
4 | import com.github.amrmsaraya.weather.domain.usecase.preferences.*
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class PreferenceModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideGetBooleanPreference(preferencesRepo: PreferencesRepo): GetBooleanPreference {
18 | return GetBooleanPreference(preferencesRepo)
19 | }
20 |
21 | @Singleton
22 | @Provides
23 | fun provideGetIntPreference(preferencesRepo: PreferencesRepo): GetIntPreference {
24 | return GetIntPreference(preferencesRepo)
25 | }
26 |
27 | @Singleton
28 | @Provides
29 | fun provideGetStringPreference(preferencesRepo: PreferencesRepo): GetStringPreference {
30 | return GetStringPreference(preferencesRepo)
31 | }
32 |
33 | @Singleton
34 | @Provides
35 | fun provideRestorePreferences(preferencesRepo: PreferencesRepo): RestorePreferences {
36 | return RestorePreferences(preferencesRepo)
37 | }
38 |
39 | @Singleton
40 | @Provides
41 | fun provideSavePreference(preferencesRepo: PreferencesRepo): SavePreference {
42 | return SavePreference(preferencesRepo)
43 | }
44 |
45 | @Singleton
46 | @Provides
47 | fun provideSetDefaultPreferences(preferencesRepo: PreferencesRepo): SetDefaultPreferences {
48 | return SetDefaultPreferences(preferencesRepo)
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.settings
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.github.amrmsaraya.weather.domain.model.Settings
7 | import com.github.amrmsaraya.weather.domain.usecase.preferences.RestorePreferences
8 | import com.github.amrmsaraya.weather.domain.usecase.preferences.SavePreference
9 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.flow.collect
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class SettingsViewModel @Inject constructor(
18 | private val savePreference: SavePreference,
19 | private val restorePreferences: RestorePreferences,
20 | private val dispatcher: IDispatchers
21 | ) : ViewModel() {
22 |
23 | init {
24 | restorePreferences()
25 | }
26 |
27 | val settings = mutableStateOf(null)
28 |
29 | fun restorePreferences() = viewModelScope.launch(dispatcher.default) {
30 | restorePreferences.execute().collect {
31 | withContext(dispatcher.main) {
32 | settings.value = it
33 | }
34 | }
35 | }
36 |
37 | fun savePreference(key: String, value: Int) = viewModelScope.launch(dispatcher.default) {
38 | savePreference.execute(key, value)
39 | }
40 |
41 | fun savePreference(key: String, value: Boolean) = viewModelScope.launch(dispatcher.default) {
42 | savePreference.execute(key, value)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/GeocoderHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import android.content.Context
4 | import android.location.Geocoder
5 | import com.github.amrmsaraya.weather.R
6 | import java.util.*
7 |
8 | object GeocoderHelper {
9 | fun getCity(context: Context, lat: Double, lon: Double): String {
10 | val geocoder = Geocoder(context, Locale.getDefault())
11 | return try {
12 | val address = geocoder.getFromLocation(lat, lon, 1).firstOrNull()
13 | return address?.let {
14 | when {
15 | !it.locality.isNullOrEmpty() -> it.locality
16 | !it.adminArea.isNullOrEmpty() -> it.adminArea
17 | !it.getAddressLine(0).isNullOrEmpty() -> it.getAddressLine(0)
18 | else -> context.getString(R.string.unknown)
19 | }
20 | } ?: context.getString(R.string.unknown)
21 | } catch (exception: Exception) {
22 | context.getString(R.string.unknown)
23 | }
24 | }
25 |
26 | fun getAddress(context: Context, lat: Double, lon: Double): String {
27 | val geocoder = Geocoder(context, Locale.getDefault())
28 | return try {
29 | val address = geocoder.getFromLocation(lat, lon, 1).firstOrNull()
30 | return address?.let {
31 | when {
32 | !address.getAddressLine(0).isNullOrEmpty() -> address.getAddressLine(0)
33 | else -> context.getString(R.string.unknown)
34 | }
35 | } ?: context.getString(R.string.unknown)
36 | } catch (exception: Exception) {
37 | context.getString(R.string.unknown)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/Repository.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import com.github.amrmsaraya.weather.data.repositoryImp.AlertRepoImp
6 | import com.github.amrmsaraya.weather.data.repositoryImp.ForecastRepoImp
7 | import com.github.amrmsaraya.weather.data.repositoryImp.PreferencesRepoImp
8 | import com.github.amrmsaraya.weather.data.source.AlertDataSource
9 | import com.github.amrmsaraya.weather.data.source.LocalDataSource
10 | import com.github.amrmsaraya.weather.data.source.RemoteDataSource
11 | import com.github.amrmsaraya.weather.domain.repository.AlertRepo
12 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
13 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
14 | import dagger.Module
15 | import dagger.Provides
16 | import dagger.hilt.InstallIn
17 | import dagger.hilt.components.SingletonComponent
18 | import javax.inject.Singleton
19 |
20 |
21 | @Module
22 | @InstallIn(SingletonComponent::class)
23 | class Repository {
24 |
25 | @Provides
26 | @Singleton
27 | fun providePreferencesRepo(dataStore: DataStore): PreferencesRepo {
28 | return PreferencesRepoImp(dataStore)
29 | }
30 |
31 | @Provides
32 | @Singleton
33 | fun provideForecastRepo(
34 | localDataSource: LocalDataSource,
35 | remoteDataSource: RemoteDataSource
36 | ): ForecastRepo {
37 | return ForecastRepoImp(localDataSource, remoteDataSource)
38 | }
39 |
40 | @Provides
41 | @Singleton
42 | fun provideAlertRepo(
43 | alertDataSource: AlertDataSource,
44 | ): AlertRepo {
45 | return AlertRepoImp(alertDataSource)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/UpdateFavoritesForecastTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Current
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
6 | import com.google.common.truth.Truth
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.test.runBlockingTest
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | @ExperimentalCoroutinesApi
13 | class UpdateFavoritesForecastTest {
14 | private lateinit var fakeForecastRepo: FakeForecastRepo
15 | private lateinit var updateFavoritesForecast: UpdateFavoritesForecast
16 |
17 | @Before
18 | fun setup() {
19 | fakeForecastRepo = FakeForecastRepo()
20 | updateFavoritesForecast = UpdateFavoritesForecast(fakeForecastRepo)
21 | }
22 |
23 | @Test
24 | fun `execute() then the favorite forecasts will be updated`() = runBlockingTest {
25 | // Given
26 | val forecasts = listOf(
27 | Forecast(id = 2, current = Current(temp = 10.0)),
28 | Forecast(id = 3, current = Current(temp = 10.0)),
29 | Forecast(id = 4, current = Current(temp = 10.0)),
30 | Forecast(id = 5, current = Current(temp = 10.0)),
31 | )
32 | forecasts.forEach {
33 | fakeForecastRepo.insertForecast(it)
34 | }
35 |
36 | // When
37 | updateFavoritesForecast.execute()
38 |
39 | // Then
40 | Truth.assertThat(fakeForecastRepo.forecastList).isEqualTo(
41 | forecasts.map { it.copy(current = Current(temp = 28.6)) }
42 | )
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
16 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
36 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/usecase/ForecastModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di.usecase
2 |
3 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
4 | import com.github.amrmsaraya.weather.domain.usecase.forecast.*
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class ForecastModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideDeleteForecast(forecastRepo: ForecastRepo): DeleteForecast {
18 | return DeleteForecast(forecastRepo)
19 | }
20 |
21 | @Singleton
22 | @Provides
23 | fun provideGetCurrentForecast(forecastRepo: ForecastRepo): GetCurrentForecast {
24 | return GetCurrentForecast(forecastRepo)
25 | }
26 |
27 | @Singleton
28 | @Provides
29 | fun provideGetFavoriteForecasts(forecastRepo: ForecastRepo): GetFavoriteForecasts {
30 | return GetFavoriteForecasts(forecastRepo)
31 | }
32 |
33 | @Singleton
34 | @Provides
35 | fun provideGetForecast(forecastRepo: ForecastRepo): GetForecast {
36 | return GetForecast(forecastRepo)
37 | }
38 |
39 | @Singleton
40 | @Provides
41 | fun provideInsertForecast(forecastRepo: ForecastRepo): InsertForecast {
42 | return InsertForecast(forecastRepo)
43 | }
44 |
45 | @Singleton
46 | @Provides
47 | fun provideGetForecastFromMap(forecastRepo: ForecastRepo): GetForecastFromMap {
48 | return GetForecastFromMap(forecastRepo)
49 | }
50 |
51 | @Singleton
52 | @Provides
53 | fun provideUpdateFavoritesForecast(forecastRepo: ForecastRepo): UpdateFavoritesForecast {
54 | return UpdateFavoritesForecast(forecastRepo)
55 | }
56 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/util/Converter.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.util
2 |
3 | import androidx.room.TypeConverter
4 | import com.github.amrmsaraya.weather.data.model.forecast.AlertDTO
5 | import com.github.amrmsaraya.weather.data.model.forecast.CurrentDTO
6 | import com.github.amrmsaraya.weather.data.model.forecast.DailyDTO
7 | import com.github.amrmsaraya.weather.data.model.forecast.HourlyDTO
8 | import kotlinx.serialization.ExperimentalSerializationApi
9 | import kotlinx.serialization.decodeFromString
10 | import kotlinx.serialization.encodeToString
11 | import kotlinx.serialization.json.Json
12 |
13 | @ExperimentalSerializationApi
14 | class Converter {
15 |
16 | @TypeConverter
17 | fun fromCurrent(current: CurrentDTO): String {
18 | return Json.encodeToString(current)
19 | }
20 |
21 | @TypeConverter
22 | fun toCurrent(string: String): CurrentDTO {
23 | return Json.decodeFromString(string)
24 | }
25 |
26 | @TypeConverter
27 | fun fromHourly(hourly: List): String {
28 | return Json.encodeToString(hourly)
29 | }
30 |
31 | @TypeConverter
32 | fun toHourly(string: String): List {
33 | return Json.decodeFromString(string)
34 | }
35 |
36 | @TypeConverter
37 | fun fromDaily(daily: List): String {
38 | return Json.encodeToString(daily)
39 | }
40 |
41 | @TypeConverter
42 | fun toDaily(string: String): List {
43 | return Json.decodeFromString(string)
44 | }
45 |
46 | @TypeConverter
47 | fun fromAlerts(alerts: List): String {
48 | return Json.encodeToString(alerts)
49 | }
50 |
51 | @TypeConverter
52 | fun toAlerts(string: String): List {
53 | return Json.decodeFromString(string)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/github/amrmsaraya/weather/domain/usecase/forecast/GetCurrentForecast.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
5 | import com.github.amrmsaraya.weather.domain.util.Response
6 |
7 | class GetCurrentForecast(private val forecastRepo: ForecastRepo) {
8 | suspend fun execute(forceUpdate: Boolean = true): Response {
9 | return runCatching {
10 | if (forceUpdate) {
11 | val cached = forecastRepo.getCurrentForecast()
12 | val response = forecastRepo.getCurrentForecast(cached.lat, cached.lon, true)
13 | forecastRepo.insertForecast(response.copy(id = 1))
14 | }
15 | Response.Success(forecastRepo.getCurrentForecast())
16 | }.getOrElse {
17 | runCatching {
18 | val response = forecastRepo.getCurrentForecast()
19 | Response.Error(it, response)
20 | }.getOrElse {
21 | Response.Error(it, null)
22 | }
23 | }
24 | }
25 |
26 | suspend fun execute(lat: Double, lon: Double): Response {
27 | return runCatching {
28 | val response = forecastRepo.getCurrentForecast(lat, lon, true)
29 | forecastRepo.insertForecast(response.copy(id = 1))
30 | val cached = forecastRepo.getCurrentForecast()
31 | Response.Success(cached)
32 | }.getOrElse {
33 | runCatching {
34 | val response = forecastRepo.getCurrentForecast()
35 | Response.Error(it, response)
36 | }.getOrElse {
37 | Response.Error(it, null)
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/InsertForecastTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
5 | import com.google.common.truth.Truth.assertThat
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | @ExperimentalCoroutinesApi
12 | class InsertForecastTest {
13 |
14 | private lateinit var fakeForecastRepo: FakeForecastRepo
15 | private lateinit var insertForecast: InsertForecast
16 |
17 | @Before
18 | fun setup() {
19 | fakeForecastRepo = FakeForecastRepo()
20 | insertForecast = InsertForecast(fakeForecastRepo)
21 | }
22 |
23 | @Test
24 | fun `execute() with forecast then it should be inserted to forecastList`() =
25 | runBlockingTest {
26 | // Given
27 | val forecast = Forecast(id = 2)
28 |
29 | // When
30 | insertForecast.execute(forecast)
31 |
32 | // Then
33 | assertThat(fakeForecastRepo.forecastList).isEqualTo(listOf(forecast))
34 | }
35 |
36 | @Test
37 | fun `execute() with same id then it should replace the old value in forecastList`() =
38 | runBlockingTest {
39 | // Given
40 | val forecast = Forecast(id = 2, lat = 30.2, lon = 31.6)
41 | val newForecast = Forecast(id = 2, lat = 20.3, lon = 21.4)
42 | insertForecast.execute(forecast)
43 |
44 | // When
45 | insertForecast.execute(newForecast)
46 |
47 | // Then
48 | assertThat(fakeForecastRepo.forecastList).isEqualTo(listOf(newForecast))
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/navigation/Screens.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.navigation
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.*
6 | import androidx.compose.material.icons.outlined.*
7 | import androidx.compose.ui.graphics.vector.ImageVector
8 | import com.github.amrmsaraya.weather.R
9 |
10 | sealed class Screens(
11 | val route: String,
12 | @StringRes val stringId: Int,
13 | val activeIcon: ImageVector,
14 | val inactiveIcon: ImageVector
15 | ) {
16 | object Home : Screens(
17 | route = "home",
18 | stringId = R.string.home,
19 | activeIcon = Icons.Filled.Home,
20 | inactiveIcon = Icons.Outlined.Home
21 | )
22 |
23 | object Favorites : Screens(
24 | route = "favourites",
25 | stringId = R.string.favorites,
26 | activeIcon = Icons.Filled.Favorite,
27 | inactiveIcon = Icons.Outlined.FavoriteBorder
28 | )
29 |
30 | object Alerts : Screens(
31 | route = "alerts",
32 | stringId = R.string.alerts,
33 | activeIcon = Icons.Filled.Notifications,
34 | inactiveIcon = Icons.Outlined.Notifications
35 | )
36 |
37 | object Settings : Screens(
38 | route = "settings",
39 | stringId = R.string.settings,
40 | activeIcon = Icons.Filled.Settings,
41 | inactiveIcon = Icons.Outlined.Settings
42 | )
43 |
44 | object FavoriteDetails : Screens(
45 | route = "favoriteDetails",
46 | stringId = R.string.favorites,
47 | activeIcon = Icons.Filled.Favorite,
48 | inactiveIcon = Icons.Outlined.FavoriteBorder
49 | )
50 |
51 | object Maps : Screens(
52 | route = "map",
53 | stringId = R.string.map,
54 | activeIcon = Icons.Filled.Map,
55 | inactiveIcon = Icons.Outlined.Map
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/repositoryImp/ForecastRepoImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.repositoryImp
2 |
3 | import com.github.amrmsaraya.weather.data.source.LocalDataSource
4 | import com.github.amrmsaraya.weather.data.source.RemoteDataSource
5 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
6 | import com.github.amrmsaraya.weather.domain.repository.ForecastRepo
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | class ForecastRepoImp(
10 | private val localDataSource: LocalDataSource,
11 | private val remoteDataSource: RemoteDataSource
12 | ) : ForecastRepo {
13 |
14 | override suspend fun insertForecast(forecast: Forecast) {
15 | localDataSource.insertForecast(forecast)
16 | }
17 |
18 | override suspend fun deleteForecast(forecast: Forecast) {
19 | localDataSource.deleteForecast(forecast)
20 | }
21 |
22 | override suspend fun deleteForecast(list: List) {
23 | localDataSource.deleteForecast(list)
24 | }
25 |
26 | override suspend fun getLocalForecast(id: Long): Forecast {
27 | return localDataSource.getForecast(id)
28 | }
29 |
30 | override suspend fun getRemoteForecast(lat: Double, lon: Double): Forecast {
31 | return remoteDataSource.getForecast(lat, lon)
32 | }
33 |
34 | override suspend fun getCurrentForecast(
35 | lat: Double,
36 | lon: Double,
37 | forceUpdate: Boolean
38 | ): Forecast {
39 | return when (forceUpdate) {
40 | true -> remoteDataSource.getForecast(lat, lon)
41 | false -> localDataSource.getCurrentForecast()
42 | }
43 | }
44 |
45 | override suspend fun getCurrentForecast(): Forecast {
46 | return localDataSource.getCurrentForecast()
47 | }
48 |
49 | override suspend fun getFavoriteForecasts(): Flow> {
50 | return localDataSource.getFavoriteForecasts()
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
15 |
19 |
20 |
24 |
25 |
29 |
30 |
34 |
35 |
39 |
40 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/GetForecastTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Current
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
6 | import com.github.amrmsaraya.weather.domain.util.Response
7 | import com.google.common.truth.Truth.assertThat
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.test.runBlockingTest
10 | import org.junit.Before
11 | import org.junit.Test
12 |
13 | @ExperimentalCoroutinesApi
14 | class GetForecastTest {
15 |
16 | private lateinit var fakeForecastRepo: FakeForecastRepo
17 | private lateinit var getForecast: GetForecast
18 |
19 | @Before
20 | fun setup() {
21 | fakeForecastRepo = FakeForecastRepo()
22 | getForecast = GetForecast(fakeForecastRepo)
23 | }
24 |
25 | @Test
26 | fun `execute() with id then get forecast from remote and save it to forecastList then return Success`() =
27 | runBlockingTest {
28 | // Given
29 | val forecast =
30 | Forecast(id = 3, lat = 30.54, lon = 45.59, current = Current(temp = 10.0))
31 | fakeForecastRepo.insertForecast(forecast)
32 |
33 | // When
34 | val result = getForecast.execute(3)
35 |
36 | // Then
37 | assertThat(result).isEqualTo(
38 | Response.Success(forecast.copy(current = Current(temp = 28.6)))
39 | )
40 | }
41 |
42 | @Test(expected = NoSuchElementException::class)
43 | fun `execute() with id that doesn't exist then get forecast from remote and save it to forecastList then return Error`() =
44 | runBlockingTest {
45 | // When
46 | val result = getForecast.execute(3)
47 |
48 | // Then
49 | if (result is Response.Error) {
50 | throw result.throwable
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
20 |
21 |
26 |
27 |
31 |
32 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clear_day.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clear_day_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/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
22 |
23 | -keepattributes *Annotation*, InnerClasses
24 | -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
25 |
26 | # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
27 | -keepclassmembers class kotlinx.serialization.json.** {
28 | *** Companion;
29 | }
30 | -keepclasseswithmembers class kotlinx.serialization.json.** {
31 | kotlinx.serialization.KSerializer serializer(...);
32 | }
33 |
34 | # Application rules
35 |
36 | # Change here com.yourcompany.yourpackage
37 | -keepclassmembers @kotlinx.serialization.Serializable class com.github.amrmsaraya.weather.** {
38 | # lookup for plugin generated serializable classes
39 | *** Companion;
40 | # lookup for serializable objects
41 | *** INSTANCE;
42 | kotlinx.serialization.KSerializer serializer(...);
43 | }
44 | # lookup for plugin generated serializable classes
45 | -if @kotlinx.serialization.Serializable class com.github.amrmsaraya.weather.**
46 | -keepclassmembers class com.github.amrmsaraya.weather.<1>$Companion {
47 | kotlinx.serialization.KSerializer serializer(...);
48 | }
49 |
50 | # Serialization supports named companions but for such classes it is necessary to add an additional rule.
51 | # This rule keeps serializer and serializable class from obfuscation. Therefore, it is recommended not to use wildcards in it, but to write rules for each such class.
52 | -keep class com.github.amrmsaraya.weather.SerializableClassWithNamedCompanion$$serializer {
53 | *** INSTANCE;
54 | }
55 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'kotlinx-serialization'
6 | }
7 |
8 | android {
9 | compileSdk 31
10 |
11 | defaultConfig {
12 | minSdk 24
13 | targetSdk 31
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | consumerProguardFiles "consumer-rules.pro"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | // Domain module
39 | implementation project(path: ':domain')
40 |
41 | def ktor_version = '1.6.4'
42 | def room_version = '2.3.0'
43 | def dataStore_version = '1.0.0'
44 |
45 | implementation 'androidx.core:core-ktx:1.6.0'
46 | implementation 'androidx.appcompat:appcompat:1.3.1'
47 | implementation 'com.google.android.material:material:1.4.0'
48 | testImplementation 'junit:junit:4.13.2'
49 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
50 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
51 | androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
52 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2"
53 | testImplementation "com.google.truth:truth:1.1.3"
54 | androidTestImplementation "com.google.truth:truth:1.1.3"
55 |
56 | // Kotlin Serialization
57 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0'
58 |
59 | // Ktor
60 | implementation "io.ktor:ktor-client-android:$ktor_version"
61 | implementation "io.ktor:ktor-client-serialization:$ktor_version"
62 | implementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
63 |
64 | // Room
65 | implementation "androidx.room:room-runtime:$room_version"
66 | implementation "androidx.room:room-ktx:$room_version"
67 | kapt "androidx.room:room-compiler:$room_version"
68 |
69 | // Preferences DataStore
70 | implementation "androidx.datastore:datastore-preferences:$dataStore_version"
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/di/Ktor.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.di
2 |
3 | import android.util.Log
4 | import com.github.amrmsaraya.weather.BuildConfig
5 | import com.github.amrmsaraya.weather.data.remote.ApiService
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import io.ktor.client.*
11 | import io.ktor.client.engine.android.*
12 | import io.ktor.client.features.*
13 | import io.ktor.client.features.json.*
14 | import io.ktor.client.features.json.serializer.*
15 | import io.ktor.client.features.logging.*
16 | import io.ktor.client.features.observer.*
17 | import io.ktor.client.request.*
18 | import io.ktor.http.*
19 | import javax.inject.Singleton
20 |
21 | @Module
22 | @InstallIn(SingletonComponent::class)
23 | class Ktor {
24 |
25 | private val client = HttpClient(Android) {
26 |
27 | install(JsonFeature) {
28 | serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
29 | prettyPrint = true
30 | isLenient = true
31 | ignoreUnknownKeys = true
32 | })
33 | }
34 |
35 | install(HttpTimeout) {
36 | connectTimeoutMillis = 5000
37 | requestTimeoutMillis = 5000
38 | socketTimeoutMillis = 5000
39 | }
40 |
41 | install(Logging) {
42 | logger = object : Logger {
43 | override fun log(message: String) {
44 | Log.v("Logger Ktor", message)
45 | }
46 | }
47 | level = LogLevel.BODY
48 | }
49 |
50 | install(ResponseObserver) {
51 | onResponse { response ->
52 | Log.d("HTTP status", "${response.status.value}")
53 | }
54 | }
55 |
56 | install(DefaultRequest) {
57 | host = BuildConfig.BASE_URL
58 | url {
59 | protocol = URLProtocol.HTTPS
60 | }
61 |
62 | parameter("appid", BuildConfig.API_KEY)
63 | parameter("exclude", "minutely")
64 |
65 | header(HttpHeaders.ContentType, ContentType.Application.Json)
66 |
67 | }
68 | }
69 |
70 | @Singleton
71 | @Provides
72 | fun provideKtorClient(): HttpClient {
73 | return client
74 | }
75 |
76 | @Singleton
77 | @Provides
78 | fun provideApiService(client: HttpClient): ApiService {
79 | return ApiService(client)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/util/LocationHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.util
2 |
3 | import android.app.Activity
4 | import android.location.Location
5 | import android.os.Looper
6 | import android.util.Log
7 | import com.google.android.gms.location.*
8 |
9 | class LocationHelper(activity: Activity, private val onLocationChange: (Location) -> Unit) {
10 | private val fusedLocationProviderClient: FusedLocationProviderClient =
11 | LocationServices.getFusedLocationProviderClient(activity)
12 |
13 | private lateinit var locationCallback: LocationCallback
14 | private lateinit var locationRequest: LocationRequest
15 |
16 | private val tag = "Location"
17 |
18 | private fun buildLocationCallback() {
19 | locationCallback = object : LocationCallback() {
20 | override fun onLocationResult(locationResult: LocationResult) {
21 | super.onLocationResult(locationResult)
22 | onLocationChange(locationResult.lastLocation)
23 | }
24 | }
25 | }
26 |
27 | private fun buildLocationRequest() {
28 | locationRequest = LocationRequest.create().apply {
29 | interval = 5000
30 | smallestDisplacement = 500f
31 | priority = LocationRequest.PRIORITY_HIGH_ACCURACY
32 | }
33 | }
34 |
35 | fun startLocationUpdates() {
36 | buildLocationRequest()
37 | buildLocationCallback()
38 | try {
39 | Log.d(tag, "Location started.")
40 | fusedLocationProviderClient.requestLocationUpdates(
41 | locationRequest,
42 | locationCallback,
43 | Looper.getMainLooper()
44 | )
45 | } catch (exception: SecurityException) {
46 | Log.e(tag, "Lost location permissions. Couldn't remove updates. $exception")
47 | }
48 | }
49 |
50 | fun stopLocationUpdates() {
51 | try {
52 | val removeTask = fusedLocationProviderClient.removeLocationUpdates(locationCallback)
53 | removeTask.addOnCompleteListener { task ->
54 | if (task.isSuccessful) {
55 | Log.d(tag, "Location Callback removed.")
56 | } else {
57 | Log.d(tag, "Failed to remove Location Callback.")
58 | }
59 | }
60 | } catch (exception: SecurityException) {
61 | Log.e(tag, "Lost location permissions. Couldn't remove updates. $exception")
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/alert.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
30 |
31 |
41 |
42 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rainy_day.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rainy_day_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloudy_day.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloudy_day_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/repository/FakeForecastRepo.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.repository
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Current
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.flow
7 | import kotlin.random.Random
8 |
9 | class FakeForecastRepo : ForecastRepo {
10 |
11 | val forecastList = mutableListOf()
12 |
13 | override suspend fun insertForecast(forecast: Forecast) {
14 |
15 | // Simulate OnConflictStrategy.REPLACE for Room
16 | val result = forecastList.map {
17 | if (it.id == forecast.id) forecast else it
18 | }
19 | if (result.toMutableList() == forecastList) {
20 | forecastList.add(forecast)
21 | } else {
22 | forecastList.clear()
23 | forecastList.addAll(result)
24 | }
25 | }
26 |
27 | override suspend fun deleteForecast(forecast: Forecast) {
28 | val result = forecastList.remove(forecast)
29 | if (!result) {
30 | throw NoSuchElementException()
31 | }
32 | }
33 |
34 | override suspend fun deleteForecast(list: List) {
35 | list.forEach {
36 | val result = forecastList.remove(it)
37 | if (!result) {
38 | throw NoSuchElementException()
39 | }
40 | }
41 | }
42 |
43 | override suspend fun getLocalForecast(id: Long): Forecast {
44 | val forecast = forecastList.firstOrNull { it.id == id }
45 | if (forecast != null) {
46 | return forecast
47 | } else {
48 | throw NoSuchElementException()
49 | }
50 | }
51 |
52 | override suspend fun getRemoteForecast(lat: Double, lon: Double): Forecast {
53 | return Forecast(
54 | lat = lat,
55 | lon = lon,
56 | current = Current(temp = 28.6)
57 | )
58 | }
59 |
60 | override suspend fun getCurrentForecast(
61 | lat: Double,
62 | lon: Double,
63 | forceUpdate: Boolean
64 | ): Forecast {
65 | return Forecast(
66 | id = 1,
67 | lat = lat,
68 | lon = lon,
69 | current = Current(temp = 28.6)
70 | )
71 | }
72 |
73 | override suspend fun getCurrentForecast(): Forecast {
74 | val forecast = forecastList.firstOrNull { it.id == 1L }
75 | if (forecast != null) {
76 | return forecast
77 | } else {
78 | throw NoSuchElementException()
79 | }
80 | }
81 |
82 | override suspend fun getFavoriteForecasts(): Flow> {
83 | return flow {
84 | emit(forecastList.filter { it.id != 1L })
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/alerts/AlertsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.alerts
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.github.amrmsaraya.weather.domain.model.Alerts
8 | import com.github.amrmsaraya.weather.domain.usecase.alert.DeleteAlert
9 | import com.github.amrmsaraya.weather.domain.usecase.alert.GetAlerts
10 | import com.github.amrmsaraya.weather.domain.usecase.alert.InsertAlert
11 | import com.github.amrmsaraya.weather.domain.usecase.preferences.GetIntPreference
12 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.collect
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class AlertsViewModel @Inject constructor(
22 | private val getIntPreference: GetIntPreference,
23 | private val insertAlert: InsertAlert,
24 | private val deleteAlert: DeleteAlert,
25 | private val getAlerts: GetAlerts,
26 | private val dispatcher: IDispatchers
27 | ) : ViewModel() {
28 |
29 | private val _uiState = mutableStateOf(AlertsUiState())
30 | val uiState: State = _uiState
31 | val intent = MutableStateFlow(AlertsIntent.Idle)
32 |
33 | init {
34 | mapIntent()
35 | intent.value = AlertsIntent.GetAccent
36 | intent.value = AlertsIntent.GetAlerts
37 | }
38 |
39 | private fun mapIntent() = viewModelScope.launch {
40 | intent.collect {
41 | when (it) {
42 | is AlertsIntent.GetAccent -> getAccent()
43 | is AlertsIntent.GetAlerts -> getAlerts()
44 | is AlertsIntent.InsertAlert -> insertAlert(it.alert)
45 | is AlertsIntent.DeleteAlerts -> deleteAlerts(it.alerts)
46 | is AlertsIntent.Idle -> Unit
47 | }
48 | }
49 | }
50 |
51 | private fun getAccent() = viewModelScope.launch(dispatcher.default) {
52 | getIntPreference.execute("accent").collect {
53 | withContext(dispatcher.main) {
54 | _uiState.value = _uiState.value.copy(accent = it)
55 | }
56 | }
57 | }
58 |
59 | private fun insertAlert(alert: Alerts) =
60 | viewModelScope.launch(dispatcher.default) {
61 | insertAlert.execute(alert)
62 | }
63 |
64 | private fun deleteAlerts(alerts: List) =
65 | viewModelScope.launch(dispatcher.default) {
66 | deleteAlert.execute(alerts)
67 | }
68 |
69 | private fun getAlerts() = viewModelScope.launch(dispatcher.default) {
70 | getAlerts.execute().collect {
71 | withContext(dispatcher.main) {
72 | _uiState.value = _uiState.value.copy(alerts = it)
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorites/FavoritesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorites
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
8 | import com.github.amrmsaraya.weather.domain.usecase.forecast.DeleteForecast
9 | import com.github.amrmsaraya.weather.domain.usecase.forecast.GetFavoriteForecasts
10 | import com.github.amrmsaraya.weather.domain.usecase.preferences.RestorePreferences
11 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.collect
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class FavoritesViewModel @Inject constructor(
22 | private val getFavoriteForecasts: GetFavoriteForecasts,
23 | private val deleteForecast: DeleteForecast,
24 | private val restorePreferences: RestorePreferences,
25 | private val dispatcher: IDispatchers
26 | ) : ViewModel() {
27 |
28 | private val _uiState = mutableStateOf(FavoritesUiState())
29 | val uiState: State = _uiState
30 | val intent = MutableStateFlow(FavoritesIntent.Idle)
31 |
32 | init {
33 | mapIntents()
34 | intent.value = FavoritesIntent.RestorePreferences
35 | intent.value = FavoritesIntent.GetFavoritesForecast
36 | }
37 |
38 | private fun mapIntents() = viewModelScope.launch {
39 | intent.collect {
40 | when (it) {
41 | is FavoritesIntent.DeleteForecasts -> deleteForecast(it.favorites)
42 | is FavoritesIntent.GetFavoritesForecast -> getFavoriteForecasts()
43 | is FavoritesIntent.RestorePreferences -> restorePreferences()
44 | is FavoritesIntent.Idle -> Unit
45 | }
46 | intent.value = FavoritesIntent.Idle
47 | }
48 | }
49 |
50 |
51 | private fun getFavoriteForecasts() = viewModelScope.launch(dispatcher.default) {
52 | val response = getFavoriteForecasts.execute()
53 | response.collect {
54 | withContext(dispatcher.main) {
55 | _uiState.value = _uiState.value.copy(favorites = it)
56 | }
57 | }
58 | }
59 |
60 | private fun deleteForecast(list: List) = viewModelScope.launch(dispatcher.default) {
61 | deleteForecast.execute(list)
62 | }
63 |
64 | private fun restorePreferences() = viewModelScope.launch(dispatcher.default) {
65 | restorePreferences.execute().collect {
66 | withContext(Dispatchers.Main) {
67 | _uiState.value = _uiState.value.copy(settings = it)
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/DeleteForecastTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
4 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
5 | import com.google.common.truth.Truth.assertThat
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.runBlockingTest
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | @ExperimentalCoroutinesApi
12 |
13 | class DeleteForecastTest {
14 |
15 | private lateinit var fakeForecastRepo: FakeForecastRepo
16 | private lateinit var deleteForecast: DeleteForecast
17 |
18 | @Before
19 | fun setup() {
20 | fakeForecastRepo = FakeForecastRepo()
21 | deleteForecast = DeleteForecast(fakeForecastRepo)
22 | }
23 |
24 | @Test
25 | fun `execute() with forecast then forecast is deleted from forecastList`() =
26 | runBlockingTest {
27 | // Given
28 | val forecast1 = Forecast(id = 2)
29 | val forecast2 = Forecast(id = 3)
30 | fakeForecastRepo.insertForecast(forecast1)
31 | fakeForecastRepo.insertForecast(forecast2)
32 |
33 | // When
34 | deleteForecast.execute(forecast2)
35 |
36 | // Then
37 | assertThat(fakeForecastRepo.forecastList).isEqualTo(mutableListOf(Forecast(id = 2)))
38 | }
39 |
40 | @Test(expected = NoSuchElementException::class)
41 | fun `execute() with forecast doesn't exist then throw NoSuchElementException`() =
42 | runBlockingTest {
43 | // Given
44 | val forecast = Forecast(id = 2)
45 | // When
46 | deleteForecast.execute(forecast)
47 |
48 | }
49 |
50 | @Test
51 | fun `execute() with forecast List then forecast list is deleted`() =
52 | runBlockingTest {
53 | // Given
54 | val forecastList =
55 | listOf(Forecast(id = 2), Forecast(id = 3), Forecast(id = 4), Forecast(id = 5))
56 |
57 | forecastList.forEach { fakeForecastRepo.insertForecast(it) }
58 |
59 | val listToDeleted = listOf(Forecast(id = 3), Forecast(id = 5))
60 |
61 | // When
62 | deleteForecast.execute(listToDeleted)
63 |
64 | // Then
65 | assertThat(fakeForecastRepo.forecastList).isEqualTo(
66 | listOf(
67 | Forecast(id = 2),
68 | Forecast(id = 4)
69 | )
70 | )
71 | }
72 |
73 | @Test(expected = NoSuchElementException::class)
74 | fun `execute() with forecast List that have items doesn't exist then throw NoSuchElementException`() =
75 | runBlockingTest {
76 | // Given
77 | val forecast = Forecast(id = 2)
78 | fakeForecastRepo.insertForecast(forecast)
79 | val listToDeleted = listOf(Forecast(id = 2), Forecast(id = 5))
80 |
81 | // When
82 | deleteForecast.execute(listToDeleted)
83 |
84 | }
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/data/src/androidTest/java/com/github/amrmsaraya/weather/data/local/AlertDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.local
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.room.Room
5 | import androidx.test.core.app.ApplicationProvider
6 | import androidx.test.ext.junit.runners.AndroidJUnit4
7 | import com.github.amrmsaraya.weather.data.model.AlertsDTO
8 | import com.github.amrmsaraya.weather.domain.model.Alerts
9 | import com.google.common.truth.Truth.assertThat
10 | import kotlinx.coroutines.flow.first
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.serialization.ExperimentalSerializationApi
13 | import org.junit.After
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 |
19 | @ExperimentalSerializationApi
20 |
21 | @RunWith(AndroidJUnit4::class)
22 | class AlertDaoTest {
23 |
24 | @get:Rule
25 | var instantTaskExecutorRule = InstantTaskExecutorRule()
26 |
27 | private lateinit var database: WeatherDatabase
28 | private lateinit var alertDao: AlertDao
29 |
30 | @Before
31 | fun initDB() {
32 | database = Room.inMemoryDatabaseBuilder(
33 | ApplicationProvider.getApplicationContext(),
34 | WeatherDatabase::class.java
35 | ).build()
36 | alertDao = database.alertDao()
37 | }
38 |
39 | @After
40 | fun closeDB() = database.close()
41 |
42 |
43 | @Test
44 | fun insertAlert() = runBlocking {
45 |
46 | // Given
47 | val alert = AlertsDTO(5, 1, 1, true, "1234")
48 |
49 | // When
50 | alertDao.insert(alert)
51 | val result = alertDao.getAlert("1234")
52 |
53 | // Then
54 | assertThat(result).isEqualTo(alert)
55 | }
56 |
57 | @Test
58 | fun deleteAlert() = runBlocking {
59 |
60 | // Given
61 | val alert = AlertsDTO(5, 1, 1, true, "1234")
62 | alertDao.insert(alert)
63 |
64 | // When
65 | alertDao.delete("1234")
66 | val result = alertDao.getAlert("1234")
67 |
68 | // Then
69 | assertThat(result).isNotEqualTo(alert)
70 | }
71 |
72 | @Test
73 | fun getAlert() = runBlocking {
74 |
75 | // Given
76 | val alert = AlertsDTO(5, 1, 1, true, "1234")
77 | alertDao.insert(alert)
78 |
79 | // When
80 | val result = alertDao.getAlert("1234")
81 |
82 | // Then
83 | assertThat(result).isEqualTo(alert)
84 | }
85 |
86 | @Test
87 | fun getAlerts() = runBlocking {
88 | database.clearAllTables()
89 |
90 | // Given
91 | val alert1 = AlertsDTO(3, 1, 1, true, "1234")
92 | val alert2 = AlertsDTO(4, 3, 3, true, "1234")
93 | alertDao.insert(alert1)
94 | alertDao.insert(alert2)
95 |
96 | // When
97 | val result = alertDao.getAlerts().first()
98 |
99 | // Then
100 | assertThat(result).isEqualTo(listOf(alert1, alert2))
101 | assertThat(alert1).isIn(result)
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorite_details/FavoriteDetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorite_details
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.github.amrmsaraya.weather.domain.usecase.forecast.GetForecast
8 | import com.github.amrmsaraya.weather.domain.usecase.preferences.RestorePreferences
9 | import com.github.amrmsaraya.weather.domain.util.Response
10 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.collect
15 | import kotlinx.coroutines.flow.first
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class FavoriteDetailsViewModel @Inject constructor(
22 | private val getForecast: GetForecast,
23 | private val restorePreferences: RestorePreferences,
24 | private val dispatcher: IDispatchers
25 | ) : ViewModel() {
26 |
27 | private val _uiState = mutableStateOf(FavoriteDetailsUiState())
28 | val uiState: State = _uiState
29 |
30 | val intent = MutableStateFlow(FavoriteDetailsIntent.Idle)
31 |
32 | init {
33 | mapIntent()
34 | intent.value = FavoriteDetailsIntent.RestorePreferences
35 | }
36 |
37 | private fun mapIntent() = viewModelScope.launch {
38 | intent.collect {
39 | when (it) {
40 | is FavoriteDetailsIntent.GetForecast -> getForecast(it.id)
41 | is FavoriteDetailsIntent.RestorePreferences -> restorePreferences()
42 | is FavoriteDetailsIntent.ClearThrowable -> clearThrowable()
43 | is FavoriteDetailsIntent.Idle -> Unit
44 | }
45 | intent.value = FavoriteDetailsIntent.Idle
46 | }
47 | }
48 |
49 | private fun getForecast(id: Long) = viewModelScope.launch(dispatcher.default) {
50 | _uiState.value = _uiState.value.copy(isLoading = true)
51 | val response = getForecast.execute(id)
52 | withContext(dispatcher.main) {
53 | _uiState.value = when (response) {
54 | is Response.Success -> _uiState.value.copy(
55 | forecast = response.result,
56 | isLoading = false
57 | )
58 | is Response.Error -> _uiState.value.copy(
59 | forecast = response.result,
60 | throwable = response.throwable,
61 | isLoading = false
62 | )
63 | }
64 | }
65 | }
66 |
67 | private fun clearThrowable() {
68 | _uiState.value = _uiState.value.copy(throwable = null)
69 | }
70 |
71 | fun restorePreferences() = viewModelScope.launch(dispatcher.default) {
72 | val settings = restorePreferences.execute().first()
73 | withContext(Dispatchers.Main) {
74 | _uiState.value = _uiState.value.copy(settings = settings)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/domain/src/test/kotlin/com/github/amrmsaraya/weather/domain/usecase/forecast/GetCurrentForecastTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.domain.usecase.forecast
2 |
3 | import com.github.amrmsaraya.weather.domain.model.forecast.Current
4 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
5 | import com.github.amrmsaraya.weather.domain.repository.FakeForecastRepo
6 | import com.github.amrmsaraya.weather.domain.util.Response
7 | import com.google.common.truth.Truth.assertThat
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.test.runBlockingTest
10 | import org.junit.Before
11 | import org.junit.Test
12 |
13 | @ExperimentalCoroutinesApi
14 | class GetCurrentForecastTest {
15 | private lateinit var fakeForecastRepo: FakeForecastRepo
16 | private lateinit var getCurrentForecast: GetCurrentForecast
17 |
18 | @Before
19 | fun setup() {
20 | fakeForecastRepo = FakeForecastRepo()
21 | getCurrentForecast = GetCurrentForecast(fakeForecastRepo)
22 | }
23 |
24 | @Test
25 | fun `execute() with no params return the current forecast with response success`() =
26 | runBlockingTest {
27 | // Given
28 | val forecast = Forecast(id = 1, current = Current(temp = 10.0))
29 | fakeForecastRepo.insertForecast(forecast)
30 |
31 | // When
32 | val result = getCurrentForecast.execute()
33 |
34 | // Then
35 | assertThat(result).isEqualTo(
36 | Response.Success(forecast.copy(current = Current(temp = 28.6)))
37 | )
38 | }
39 |
40 | @Test(expected = NoSuchElementException::class)
41 | fun `execute() with no params when current forecast isn't exist then throw NoSuchElementException`() =
42 | runBlockingTest {
43 | // When
44 | val result = getCurrentForecast.execute()
45 | if (result is Response.Error) {
46 | throw result.throwable
47 | }
48 | }
49 |
50 |
51 | @Test
52 | fun `execute() with lat & lon return the current forecast with response success`() =
53 | runBlockingTest {
54 | // Given
55 | val forecast = Forecast(id = 1, lat = 31.0, lon = 41.0, current = Current(temp = 10.0))
56 | fakeForecastRepo.insertForecast(forecast)
57 |
58 | // When
59 | val result = getCurrentForecast.execute(31.0, 41.0)
60 |
61 | // Then
62 | assertThat(result).isEqualTo(
63 | Response.Success(forecast.copy(current = Current(temp = 28.6)))
64 | )
65 | }
66 |
67 |
68 | @Test
69 | fun `execute() with lat & lon when current forecast isn't exist return the current forecast with response success`() =
70 | runBlockingTest {
71 | // When
72 | val result = getCurrentForecast.execute(31.0, 41.0)
73 |
74 | // Then
75 | assertThat(result).isEqualTo(
76 | Response.Success(
77 | Forecast(
78 | id = 1,
79 | lat = 31.0,
80 | lon = 41.0,
81 | current = Current(temp = 28.6)
82 | )
83 | )
84 | )
85 | }
86 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Weather - [Google Play](https://play.google.com/store/apps/details?id=com.github.amrmsaraya.weather)
6 | Android mobile application that inform user about the current weather condition in a selected location using GPS or choose a place from google-maps, user can add favorite places which can be accessed any time to check weather condition, default notifications for weather alerts which can be disabled, user can add custom alarms in a specific time range to be informed if there is any alerts within this time period, user can change current location provider, application language or temeprature and windspeed units any time.
7 |
8 | ## Features
9 | - Real-Time weather condition with full details
10 | - Favorite places to save places which user regularly check
11 | - Automatic notifications about weather alerts
12 | - Custom alarms in a specific time range to be informed if there is any alerts within this time period
13 | - Change location provider (GPS, Google-Maps Location)
14 | - Light / Dark theme support
15 | - 6 Colorful palettes to choose from
16 | - Change application language (English, Arabic)
17 | - Change temperature unit (Celsius, Kelvin, Fahrenheit)
18 | - Change wind speed unit (m/s, mph)
19 |
20 | ## Libraries and Frameworks
21 |
22 | - [Jetpack Compose](https://developer.android.com/jetpack/compose?) - Declarative UI Framework
23 | - [Material Design](https://material.io/design) - Design System
24 | - [Splash Screen](https://developer.android.com/reference/android/window/SplashScreen) - Newly introduced splash screen API
25 | - [Coroutines Flows](https://kotlinlang.org/docs/reference/coroutines/flow.html) - Reactive Programming
26 | - [Ktor](https://ktor.io/) - HTTP Client
27 | - [Kotlin Serlization](https://github.com/Kotlin/kotlinx.serialization) - Kotlin Multiplatform Serialization
28 | - [Room](https://developer.android.com/jetpack/androidx/releases/room) - Local Database
29 | - [Hilt](http://google.github.io/hilt/) - Dependency Injection
30 | - [Work Manager](https://developer.android.com/reference/androidx/work/WorkManager) - Schedule background tasks
31 | - [Coil](https://coil-kt.github.io/coil/compose) - Image Loading
32 | - [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) - Asynchronous data storage
33 | - [Lottie](https://github.com/airbnb/lottie-android) - Animation
34 | - [Google Maps](https://developers.google.com/maps/documentation/android-sdk/start) - Embedded Google Maps
35 | - [JUnit](https://junit.org/junit4/) - Unit Testing
36 | - [Truth](https://truth.dev/) - Fluent Assertions
37 |
38 | ## Architecture and Design Patterns
39 | - [Clean Architecture](https://koenig-media.raywenderlich.com/uploads/2019/02/Clean-Architecture-Bob-650x454.png) - Application architecture pattern
40 | - :app module - Presentation layer that contains UI related code and dependency injection
41 | - :data module - Data layer that contains DAOs, DTOs, Mapper, Http services, Data sources and Repository Implementation
42 | - :domain module - Business layer that contains Repository interfaces and Models (Entities)
43 | - [MVI](https://miro.medium.com/max/5152/1*iFis87B9sIfpsgQeFkgu8Q.png) - Model-View-Intent design pattern
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/favorite_details/FavoriteDetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.favorite_details
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.Scaffold
6 | import androidx.compose.material.rememberScaffoldState
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.res.stringResource
10 | import com.github.amrmsaraya.weather.presentation.components.LoadingIndicator
11 | import com.github.amrmsaraya.weather.presentation.home.HomeContent
12 | import com.github.amrmsaraya.weather.presentation.home.NoInternetConnection
13 | import com.github.amrmsaraya.weather.util.toStringResource
14 | import com.google.accompanist.swiperefresh.SwipeRefresh
15 | import com.google.accompanist.swiperefresh.SwipeRefreshIndicator
16 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
17 | import kotlinx.coroutines.launch
18 |
19 | @ExperimentalAnimationApi
20 | @Composable
21 | fun FavoriteDetailsScreen(
22 | modifier: Modifier,
23 | id: Long,
24 | viewModel: FavoriteDetailsViewModel
25 | ) {
26 | val uiState by viewModel.uiState
27 | var swipeRefresh by remember { mutableStateOf(false) }
28 | val swipeRefreshState =
29 | rememberSwipeRefreshState(if (!uiState.isLoading) uiState.isLoading else swipeRefresh)
30 | val scaffoldState = rememberScaffoldState()
31 | val scope = rememberCoroutineScope()
32 |
33 | LaunchedEffect(key1 = true) {
34 | viewModel.intent.value = FavoriteDetailsIntent.GetForecast(id)
35 | }
36 |
37 | uiState.settings?.let { setting ->
38 | Scaffold(
39 | modifier = modifier,
40 | scaffoldState = scaffoldState
41 | ) {
42 | uiState.throwable?.toStringResource()?.let {
43 | val error = stringResource(it)
44 | scope.launch {
45 | viewModel.intent.value = FavoriteDetailsIntent.ClearThrowable
46 | scaffoldState.snackbarHostState.showSnackbar(error)
47 | }
48 | }
49 |
50 | SwipeRefresh(
51 | state = swipeRefreshState,
52 | onRefresh = {
53 | swipeRefresh = true
54 | viewModel.intent.value = FavoriteDetailsIntent.GetForecast(id)
55 | },
56 | indicator = { state, trigger ->
57 | SwipeRefreshIndicator(
58 | state = state,
59 | refreshTriggerDistance = trigger,
60 | scale = true,
61 | contentColor = MaterialTheme.colors.secondary
62 | )
63 | },
64 | ) {
65 | uiState.forecast?.let {
66 | when (it.current.weather.isNotEmpty()) {
67 | true -> HomeContent(it, setting)
68 | false -> NoInternetConnection {
69 | viewModel.intent.value = FavoriteDetailsIntent.GetForecast(id)
70 | }
71 | }
72 | } ?: LoadingIndicator()
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloudy_night.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
36 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloudy_night_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
36 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/foggy_day.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/foggy_day_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/service/AlertWorker.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.service
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.Bundle
7 | import androidx.hilt.work.HiltWorker
8 | import androidx.work.CoroutineWorker
9 | import androidx.work.WorkerParameters
10 | import com.github.amrmsaraya.weather.R
11 | import com.github.amrmsaraya.weather.domain.model.forecast.Alert
12 | import com.github.amrmsaraya.weather.domain.usecase.alert.DeleteAlert
13 | import com.github.amrmsaraya.weather.domain.usecase.alert.GetAlert
14 | import com.github.amrmsaraya.weather.domain.usecase.forecast.GetCurrentForecast
15 | import com.github.amrmsaraya.weather.domain.util.Response
16 | import com.github.amrmsaraya.weather.util.NotificationHelper
17 | import dagger.assisted.Assisted
18 | import dagger.assisted.AssistedInject
19 |
20 | @HiltWorker
21 | class AlertWorker @AssistedInject constructor(
22 | @Assisted private val context: Context,
23 | @Assisted params: WorkerParameters,
24 | private val getCurrentForecast: GetCurrentForecast,
25 | private val getAlert: GetAlert,
26 | private val deleteAlert: DeleteAlert,
27 | ) : CoroutineWorker(context, params) {
28 |
29 | private val notificationHelper = NotificationHelper(context)
30 |
31 | override suspend fun doWork(): Result {
32 |
33 | var alerts = listOf()
34 | val bundle = Bundle()
35 | val intent = Intent(context, AlertService::class.java)
36 | val alert = getAlert.execute(id.toString())
37 |
38 | when (val response = getCurrentForecast.execute()) {
39 | is Response.Success -> alerts = response.result.alerts
40 | is Response.Error -> response.result?.let {
41 | alerts = it.alerts
42 | }
43 | }
44 |
45 | when (alert.isAlarm) {
46 | true -> {
47 | if (alerts.isNotEmpty()) {
48 | bundle.putString("event", alerts[0].event)
49 | bundle.putString("description", alerts[0].description)
50 | } else {
51 | bundle.putString("event", context.getString(R.string.weather_alert))
52 | bundle.putString("description", context.getString(R.string.weather_is_fine))
53 | }
54 |
55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
56 | context.startForegroundService(intent.putExtras(bundle))
57 | } else {
58 | context.startService(intent.putExtras(bundle))
59 | }
60 | }
61 | false -> {
62 | val notification = notificationHelper.getNotification(
63 | event = if (alerts.isNotEmpty()) {
64 | alerts[0].event
65 | } else {
66 | context.getString(R.string.weather_alert)
67 | },
68 | description = if (alerts.isNotEmpty()) {
69 | alerts[0].description
70 | } else {
71 | context.getString(R.string.weather_is_fine)
72 | },
73 | )
74 | notificationHelper.manager.notify(1, notification)
75 | }
76 | }
77 |
78 | deleteAlert.execute(id.toString())
79 | return Result.success()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/data/src/main/java/com/github/amrmsaraya/weather/data/repositoryImp/PreferencesRepoImp.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.data.repositoryImp
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.*
5 | import com.github.amrmsaraya.weather.domain.model.Settings
6 | import com.github.amrmsaraya.weather.domain.repository.PreferencesRepo
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 |
10 | class PreferencesRepoImp(private val dataStore: DataStore) : PreferencesRepo {
11 |
12 | override suspend fun savePreference(key: String, value: Int) {
13 | val preferencesKey = intPreferencesKey(key)
14 | dataStore.edit { settings ->
15 | settings[preferencesKey] = value
16 | }
17 | }
18 |
19 | override suspend fun savePreference(key: String, value: Boolean) {
20 | val preferencesKey = booleanPreferencesKey(key)
21 | dataStore.edit { settings ->
22 | settings[preferencesKey] = value
23 | }
24 | }
25 |
26 | override suspend fun savePreference(key: String, value: String) {
27 | val preferencesKey = stringPreferencesKey(key)
28 | dataStore.edit { settings ->
29 | settings[preferencesKey] = value
30 | }
31 | }
32 |
33 | override suspend fun getIntPreference(key: String): Flow {
34 | val preferencesKey = intPreferencesKey(key)
35 | return dataStore.data.map { preferences ->
36 | preferences[preferencesKey] ?: 0
37 | }
38 | }
39 |
40 | override suspend fun getBooleanPreference(key: String): Flow {
41 | val preferencesKey = booleanPreferencesKey(key)
42 | return dataStore.data.map { preferences ->
43 | preferences[preferencesKey] ?: true
44 | }
45 | }
46 |
47 | override suspend fun getStringPreference(key: String): Flow {
48 | val preferencesKey = stringPreferencesKey(key)
49 | return dataStore.data.map { preferences ->
50 | preferences[preferencesKey] ?: ""
51 | }
52 | }
53 |
54 | override suspend fun setDefaultPreferences(settings: Settings) {
55 | savePreference("location", settings.location)
56 | savePreference("language", settings.language)
57 | savePreference("theme", settings.theme)
58 | savePreference("accent", settings.accent)
59 | savePreference("notifications", settings.notifications)
60 | savePreference("temperature", settings.temperature)
61 | savePreference("windSpeed", settings.windSpeed)
62 | savePreference("versionCode", settings.versionCode)
63 | }
64 |
65 | override suspend fun restorePreferences(): Flow {
66 | return dataStore.data.map { preferences ->
67 | Settings(
68 | location = preferences[intPreferencesKey("location")] ?: 0,
69 | language = preferences[intPreferencesKey("language")] ?: 0,
70 | theme = preferences[intPreferencesKey("theme")] ?: 0,
71 | accent = preferences[intPreferencesKey("accent")] ?: 0,
72 | notifications = preferences[booleanPreferencesKey("notifications")] ?: true,
73 | temperature = preferences[intPreferencesKey("temperature")] ?: 0,
74 | windSpeed = preferences[intPreferencesKey("windSpeed")] ?: 0,
75 | versionCode = preferences[intPreferencesKey("versionCode")] ?: 0,
76 | )
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/presentation/map/MapViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.presentation.map
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.github.amrmsaraya.weather.domain.model.forecast.Forecast
8 | import com.github.amrmsaraya.weather.util.enums.Location
9 | import com.github.amrmsaraya.weather.domain.usecase.forecast.GetCurrentForecast
10 | import com.github.amrmsaraya.weather.domain.usecase.forecast.GetForecastFromMap
11 | import com.github.amrmsaraya.weather.domain.usecase.forecast.InsertForecast
12 | import com.github.amrmsaraya.weather.domain.usecase.preferences.SavePreference
13 | import com.github.amrmsaraya.weather.domain.util.Response
14 | import com.github.amrmsaraya.weather.util.dispatchers.IDispatchers
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.flow.MutableStateFlow
17 | import kotlinx.coroutines.flow.collect
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.withContext
20 | import javax.inject.Inject
21 |
22 | @HiltViewModel
23 | class MapViewModel @Inject constructor(
24 | private val savePreference: SavePreference,
25 | private val insertForecast: InsertForecast,
26 | private val getForecastFromMap: GetForecastFromMap,
27 | private val getCurrentForecast: GetCurrentForecast,
28 | private val dispatcher: IDispatchers,
29 | ) : ViewModel() {
30 | private val _uiState = mutableStateOf(MapUiState())
31 | val uiState: State = _uiState
32 | val intent = MutableStateFlow(MapIntent.Idle)
33 |
34 | init {
35 | mapIntent()
36 | intent.value = MapIntent.GetCurrentForecast
37 | }
38 |
39 | private fun mapIntent() = viewModelScope.launch {
40 | intent.collect {
41 | when (it) {
42 | is MapIntent.GetCurrentForecast -> getCurrentForecast()
43 | is MapIntent.GetForecastFromMap -> {
44 | _uiState.value =
45 | _uiState.value.copy(isLoading = getForecastFromMap(it.lat, it.lon))
46 | }
47 | is MapIntent.InsertForecast -> {
48 | _uiState.value = _uiState.value.copy(isLoading = insertForecast(it.forecast))
49 | }
50 | is MapIntent.Idle -> Unit
51 | }
52 | intent.value = MapIntent.Idle
53 | }
54 | }
55 |
56 | private fun insertForecast(forecast: Forecast) = viewModelScope.launch(dispatcher.default) {
57 | insertForecast.execute(forecast)
58 | savePreference.execute("location", Location.MAP.ordinal)
59 | _uiState.value = _uiState.value.copy(isLoading = null)
60 | }
61 |
62 | private fun getForecastFromMap(lat: Double, lon: Double) =
63 | viewModelScope.launch(dispatcher.default) {
64 | getForecastFromMap.execute(lat, lon)
65 | _uiState.value = _uiState.value.copy(isLoading = null)
66 | }
67 |
68 | private fun getCurrentForecast() = viewModelScope.launch(dispatcher.default) {
69 | val response = getCurrentForecast.execute(false)
70 | withContext(dispatcher.main) {
71 | _uiState.value = when (response) {
72 | is Response.Success -> _uiState.value.copy(forecast = response.result)
73 | is Response.Error -> _uiState.value.copy(
74 | forecast = response.result ?: Forecast(),
75 | throwable = response.throwable
76 | )
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/amrmsaraya/weather/service/AlertService.kt:
--------------------------------------------------------------------------------
1 | package com.github.amrmsaraya.weather.service
2 |
3 | import android.app.Service
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.graphics.PixelFormat
7 | import android.media.MediaPlayer
8 | import android.os.Build
9 | import android.os.IBinder
10 | import android.view.Gravity
11 | import android.view.LayoutInflater
12 | import android.view.View
13 | import android.view.WindowManager
14 | import android.widget.Button
15 | import android.widget.TextView
16 | import com.github.amrmsaraya.weather.R
17 | import com.github.amrmsaraya.weather.util.NotificationHelper
18 |
19 | class AlertService : Service() {
20 |
21 | private lateinit var view: View
22 | private lateinit var windowManager: WindowManager
23 | private lateinit var params: WindowManager.LayoutParams
24 | private lateinit var mediaPlayer: MediaPlayer
25 | private lateinit var notificationHelper: NotificationHelper
26 |
27 | override fun onBind(intent: Intent?): IBinder? {
28 | throw UnsupportedOperationException("Not yet implemented")
29 | }
30 |
31 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
32 |
33 | notificationHelper = NotificationHelper(this)
34 |
35 | windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
36 | val screenWidth = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
37 | windowManager.maximumWindowMetrics.bounds.width()
38 | } else {
39 | windowManager.defaultDisplay.width
40 | }
41 |
42 | val event = intent?.getStringExtra("event") ?: "Unknown"
43 | val description = intent?.getStringExtra("description") ?: "Unknown"
44 |
45 | val notification = notificationHelper.getNotification(event, description)
46 |
47 | startForeground(1, notification)
48 |
49 | val layoutInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
50 | view = layoutInflater.inflate(R.layout.alert, null)
51 |
52 | val title = view.findViewById(R.id.tvTitle)
53 | title.text = event
54 | view.findViewById(R.id.tvDescription).text = description
55 | view.findViewById