├── .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 | 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 | 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 |