├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ └── styles.xml
│ │ │ ├── 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
│ │ │ ├── drawable
│ │ │ │ ├── ic_calendar_week.xml
│ │ │ │ ├── ic_today.xml
│ │ │ │ ├── ic_weather_sunny.xml
│ │ │ │ ├── ic_settings.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── menu
│ │ │ │ └── bottom_nav.xml
│ │ │ ├── xml
│ │ │ │ └── preferences.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── future_list_weather_fragment.xml
│ │ │ │ ├── item_future_weather.xml
│ │ │ │ ├── future_detail_weather_fragment.xml
│ │ │ │ └── current_weather_fragment.xml
│ │ │ ├── navigation
│ │ │ │ └── mobile_navigation.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── resocoder
│ │ │ │ └── forecastmvvm
│ │ │ │ ├── internal
│ │ │ │ ├── UnitSystem.kt
│ │ │ │ ├── Exceptions.kt
│ │ │ │ ├── glide
│ │ │ │ │ └── ForecastAppGlideModule.kt
│ │ │ │ ├── Delegates.kt
│ │ │ │ └── TaskDeferred.kt
│ │ │ │ ├── data
│ │ │ │ ├── db
│ │ │ │ │ ├── entity
│ │ │ │ │ │ ├── Condition.kt
│ │ │ │ │ │ ├── FutureWeatherEntry.kt
│ │ │ │ │ │ ├── WeatherLocation.kt
│ │ │ │ │ │ ├── Day.kt
│ │ │ │ │ │ └── CurrentWeatherEntry.kt
│ │ │ │ │ ├── unitlocalized
│ │ │ │ │ │ ├── future
│ │ │ │ │ │ │ ├── list
│ │ │ │ │ │ │ │ ├── UnitSpecificSimpleFutureWeatherEntry.kt
│ │ │ │ │ │ │ │ ├── ImperialSimpleFutureWeatherEntry.kt
│ │ │ │ │ │ │ │ └── MetricSimpleFutureWeatherEntry.kt
│ │ │ │ │ │ │ └── detail
│ │ │ │ │ │ │ │ ├── UnitSpecificDetailFutureWeatherEntry.kt
│ │ │ │ │ │ │ │ ├── MetricDetailFutureWeatherEntry.kt
│ │ │ │ │ │ │ │ └── ImperialDetailFutureWeatherEntry.kt
│ │ │ │ │ │ └── current
│ │ │ │ │ │ │ ├── UnitSpecificCurrentWeatherEntry.kt
│ │ │ │ │ │ │ ├── MetricCurrentWeatherEntry.kt
│ │ │ │ │ │ │ └── ImperialCurrentWeatherEntry.kt
│ │ │ │ │ ├── LocalDateConverter.kt
│ │ │ │ │ ├── WeatherLocationDao.kt
│ │ │ │ │ ├── CurrentWeatherDao.kt
│ │ │ │ │ ├── ForecastDatabase.kt
│ │ │ │ │ └── FutureWeatherDao.kt
│ │ │ │ ├── network
│ │ │ │ │ ├── ConnectivityInterceptor.kt
│ │ │ │ │ ├── response
│ │ │ │ │ │ ├── ForecastDaysContainer.kt
│ │ │ │ │ │ ├── FutureWeatherResponse.kt
│ │ │ │ │ │ └── CurrentWeatherResponse.kt
│ │ │ │ │ ├── WeatherNetworkDataSource.kt
│ │ │ │ │ ├── ConnectivityInterceptorImpl.kt
│ │ │ │ │ ├── WeatherNetworkDataSourceImpl.kt
│ │ │ │ │ └── ApixuWeatherApiService.kt
│ │ │ │ ├── provider
│ │ │ │ │ ├── UnitProvider.kt
│ │ │ │ │ ├── LocationProvider.kt
│ │ │ │ │ ├── PreferenceProvider.kt
│ │ │ │ │ ├── UnitProviderImpl.kt
│ │ │ │ │ └── LocationProviderImpl.kt
│ │ │ │ └── repository
│ │ │ │ │ ├── ForecastRepository.kt
│ │ │ │ │ └── ForecastRepositoryImpl.kt
│ │ │ │ ├── ui
│ │ │ │ ├── weather
│ │ │ │ │ ├── current
│ │ │ │ │ │ ├── CurrentWeatherViewModelFactory.kt
│ │ │ │ │ │ ├── CurrentWeatherViewModel.kt
│ │ │ │ │ │ └── CurrentWeatherFragment.kt
│ │ │ │ │ └── future
│ │ │ │ │ │ ├── detail
│ │ │ │ │ │ ├── FutureDetailWeatherViewModel.kt
│ │ │ │ │ │ ├── FutureDetailViewModelFactory.kt
│ │ │ │ │ │ └── FutureDetailWeatherFragment.kt
│ │ │ │ │ │ └── list
│ │ │ │ │ │ ├── FutureListWeatherViewModel.kt
│ │ │ │ │ │ ├── FutureListWeatherViewModelFactory.kt
│ │ │ │ │ │ ├── FutureWeatherItem.kt
│ │ │ │ │ │ └── FutureListWeatherFragment.kt
│ │ │ │ ├── base
│ │ │ │ │ ├── ScopedFragment.kt
│ │ │ │ │ └── WeatherViewModel.kt
│ │ │ │ ├── settings
│ │ │ │ │ └── SettingsFragment.kt
│ │ │ │ ├── LifecycleBoundLocationManager.kt
│ │ │ │ └── MainActivity.kt
│ │ │ │ └── ForecastApplication.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── resocoder
│ │ │ └── forecastmvvm
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── resocoder
│ │ └── forecastmvvm
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── vcs.xml
├── misc.xml
├── runConfigurations.xml
├── gradle.xml
└── codeStyles
│ └── Project.xml
├── .gitignore
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Forecast MVVM
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/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/ResoCoder/forecast-mvvm-android-kotlin/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/ResoCoder/forecast-mvvm-android-kotlin/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/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/internal/UnitSystem.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.internal
2 |
3 |
4 | enum class UnitSystem {
5 | METRIC, IMPERIAL
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ResoCoder/forecast-mvvm-android-kotlin/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/entity/Condition.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.entity
2 |
3 | data class Condition(
4 | val text: String,
5 | val icon: String,
6 | val code: Int
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/ConnectivityInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network
2 |
3 | import okhttp3.Interceptor
4 |
5 |
6 | interface ConnectivityInterceptor : Interceptor
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/provider/UnitProvider.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.provider
2 |
3 | import com.resocoder.forecastmvvm.internal.UnitSystem
4 |
5 |
6 | interface UnitProvider {
7 | fun getUnitSystem(): UnitSystem
8 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Oct 20 20:03:06 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/internal/Exceptions.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.internal
2 |
3 | import java.io.IOException
4 |
5 |
6 | class NoConnectivityException: IOException()
7 | class LocationPermissionNotGrantedException: Exception()
8 | class DateNotFoundException: Exception()
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/internal/glide/ForecastAppGlideModule.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.internal.glide
2 |
3 | import com.bumptech.glide.annotation.GlideModule
4 | import com.bumptech.glide.module.AppGlideModule
5 |
6 |
7 | @GlideModule
8 | class ForecastAppGlideModule : AppGlideModule()
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/provider/LocationProvider.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.provider
2 |
3 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
4 |
5 |
6 | interface LocationProvider {
7 | suspend fun hasLocationChanged(lastWeatherLocation: WeatherLocation): Boolean
8 | suspend fun getPreferredLocationString(): String
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/internal/Delegates.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.internal
2 |
3 | import kotlinx.coroutines.*
4 |
5 |
6 | fun lazyDeferred(block: suspend CoroutineScope.() -> T): Lazy> {
7 | return lazy {
8 | GlobalScope.async(start = CoroutineStart.LAZY) {
9 | block.invoke(this)
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/response/ForecastDaysContainer.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network.response
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import com.resocoder.forecastmvvm.data.db.entity.FutureWeatherEntry
5 |
6 | data class ForecastDaysContainer(
7 | @SerializedName("forecastday")
8 | val entries: List
9 | )
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - METRIC
6 | - IMPERIAL
7 |
8 |
9 |
10 | - Metric
11 | - Imperial
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/list/UnitSpecificSimpleFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.list
2 |
3 | import org.threeten.bp.LocalDate
4 |
5 |
6 | interface UnitSpecificSimpleFutureWeatherEntry {
7 | val date: LocalDate
8 | val avgTemperature: Double
9 | val conditionText: String
10 | val conditionIconUrl: String
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/response/FutureWeatherResponse.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network.response
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
5 |
6 | data class FutureWeatherResponse(
7 | @SerializedName("forecast")
8 | val futureWeatherEntries: ForecastDaysContainer,
9 | val location: WeatherLocation
10 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_calendar_week.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/resocoder/forecastmvvm/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/current/UnitSpecificCurrentWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.current
2 |
3 |
4 | interface UnitSpecificCurrentWeatherEntry {
5 | val temperature: Double
6 | val conditionText: String
7 | val conditionIconUrl: String
8 | val windSpeed: Double
9 | val windDirection: String
10 | val precipitationVolume: Double
11 | val feelsLikeTemperature: Double
12 | val visibilityDistance: Double
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/response/CurrentWeatherResponse.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network.response
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import com.resocoder.forecastmvvm.data.db.entity.CurrentWeatherEntry
5 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
6 |
7 | data class CurrentWeatherResponse(
8 | val location: WeatherLocation,
9 | @SerializedName("current")
10 | val currentWeatherEntry: CurrentWeatherEntry
11 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_today.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/provider/PreferenceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.provider
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.preference.PreferenceManager
6 |
7 |
8 | abstract class PreferenceProvider(context: Context) {
9 | private val appContext = context.applicationContext
10 |
11 | protected val preferences: SharedPreferences
12 | get() = PreferenceManager.getDefaultSharedPreferences(appContext)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/entity/FutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.entity
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Entity
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 |
8 | @Entity(tableName = "future_weather", indices = [Index(value = ["date"], unique = true)])
9 | data class FutureWeatherEntry(
10 | @PrimaryKey(autoGenerate = true)
11 | val id: Int? = null,
12 | val date: String,
13 | @Embedded
14 | val day: Day
15 | )
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/provider/UnitProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.provider
2 |
3 | import android.content.Context
4 | import com.resocoder.forecastmvvm.internal.UnitSystem
5 |
6 | const val UNIT_SYSTEM = "UNIT_SYSTEM"
7 |
8 | class UnitProviderImpl(context: Context) : PreferenceProvider(context), UnitProvider {
9 |
10 | override fun getUnitSystem(): UnitSystem {
11 | val selectedName = preferences.getString(UNIT_SYSTEM, UnitSystem.METRIC.name)
12 | return UnitSystem.valueOf(selectedName!!)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/LocalDateConverter.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db
2 |
3 | import androidx.room.TypeConverter
4 | import org.threeten.bp.LocalDate
5 | import org.threeten.bp.format.DateTimeFormatter
6 |
7 |
8 | object LocalDateConverter {
9 | @TypeConverter
10 | @JvmStatic
11 | fun stringToDate(str: String?) = str?.let {
12 | LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
13 | }
14 |
15 | @TypeConverter
16 | @JvmStatic
17 | fun dateToString(dateTime: LocalDate?) = dateTime?.format(DateTimeFormatter.ISO_LOCAL_DATE)
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/internal/TaskDeferred.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.internal
2 |
3 | import com.google.android.gms.tasks.Task
4 | import kotlinx.coroutines.CompletableDeferred
5 | import kotlinx.coroutines.Deferred
6 |
7 |
8 | fun Task.asDeferred(): Deferred {
9 | val deferred = CompletableDeferred()
10 |
11 | this.addOnSuccessListener { result ->
12 | deferred.complete(result)
13 | }
14 |
15 | this.addOnFailureListener { exception ->
16 | deferred.completeExceptionally(exception)
17 | }
18 |
19 | return deferred
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/detail/UnitSpecificDetailFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail
2 |
3 | import org.threeten.bp.LocalDate
4 |
5 |
6 | interface UnitSpecificDetailFutureWeatherEntry {
7 | val date: LocalDate
8 | val maxTemperature: Double
9 | val minTemperature: Double
10 | val avgTemperature: Double
11 | val conditionText: String
12 | val conditionIconUrl: String
13 | val maxWindSpeed: Double
14 | val totalPrecipitation: Double
15 | val avgVisibilityDistance: Double
16 | val uv: Double
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/list/ImperialSimpleFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.list
2 |
3 | import androidx.room.ColumnInfo
4 | import org.threeten.bp.LocalDate
5 |
6 |
7 | class ImperialSimpleFutureWeatherEntry (
8 | @ColumnInfo(name = "date")
9 | override val date: LocalDate,
10 | @ColumnInfo(name = "avgtempF")
11 | override val avgTemperature: Double,
12 | @ColumnInfo(name = "condition_text")
13 | override val conditionText: String,
14 | @ColumnInfo(name = "condition_icon")
15 | override val conditionIconUrl: String
16 | ) : UnitSpecificSimpleFutureWeatherEntry
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/list/MetricSimpleFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.list
2 |
3 | import androidx.room.ColumnInfo
4 | import org.threeten.bp.LocalDate
5 |
6 |
7 | data class MetricSimpleFutureWeatherEntry (
8 | @ColumnInfo(name = "date")
9 | override val date: LocalDate,
10 | @ColumnInfo(name = "avgtempC")
11 | override val avgTemperature: Double,
12 | @ColumnInfo(name = "condition_text")
13 | override val conditionText: String,
14 | @ColumnInfo(name = "condition_icon")
15 | override val conditionIconUrl: String
16 | ) : UnitSpecificSimpleFutureWeatherEntry
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/WeatherNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.resocoder.forecastmvvm.data.network.response.CurrentWeatherResponse
5 | import com.resocoder.forecastmvvm.data.network.response.FutureWeatherResponse
6 |
7 |
8 | interface WeatherNetworkDataSource {
9 | val downloadedCurrentWeather: LiveData
10 | val downloadedFutureWeather: LiveData
11 |
12 | suspend fun fetchCurrentWeather(
13 | location: String,
14 | languageCode: String
15 | )
16 | suspend fun fetchFutureWeather(
17 | location: String,
18 | languageCode: String
19 | )
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/current/CurrentWeatherViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.current
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
6 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
7 |
8 |
9 | class CurrentWeatherViewModelFactory(
10 | private val forecastRepository: ForecastRepository,
11 | private val unitProvider: UnitProvider
12 | ) : ViewModelProvider.NewInstanceFactory() {
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | override fun create(modelClass: Class): T {
16 | return CurrentWeatherViewModel(forecastRepository, unitProvider) as T
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/resocoder/forecastmvvm/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.resocoder.forecastmvvm", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/base/ScopedFragment.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.base
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlin.coroutines.CoroutineContext
9 |
10 |
11 | abstract class ScopedFragment : Fragment(), CoroutineScope {
12 | private lateinit var job: Job
13 |
14 | override val coroutineContext: CoroutineContext
15 | get() = job + Dispatchers.Main
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | job = Job()
20 | }
21 |
22 | override fun onDestroy() {
23 | super.onDestroy()
24 | job.cancel()
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/current/CurrentWeatherViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.current
2 |
3 | import androidx.lifecycle.ViewModel;
4 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
5 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
6 | import com.resocoder.forecastmvvm.internal.UnitSystem
7 | import com.resocoder.forecastmvvm.internal.lazyDeferred
8 | import com.resocoder.forecastmvvm.ui.base.WeatherViewModel
9 |
10 | class CurrentWeatherViewModel(
11 | private val forecastRepository: ForecastRepository,
12 | unitProvider: UnitProvider
13 | ) : WeatherViewModel(forecastRepository, unitProvider) {
14 |
15 | val weather by lazyDeferred {
16 | forecastRepository.getCurrentWeather(super.isMetricUnit)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.settings
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.preference.PreferenceFragmentCompat
6 | import com.resocoder.forecastmvvm.R
7 |
8 |
9 | class SettingsFragment : PreferenceFragmentCompat() {
10 |
11 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
12 | addPreferencesFromResource(R.xml.preferences)
13 | }
14 |
15 | override fun onActivityCreated(savedInstanceState: Bundle?) {
16 | super.onActivityCreated(savedInstanceState)
17 | (activity as? AppCompatActivity)?.supportActionBar?.title = "Settings"
18 | (activity as? AppCompatActivity)?.supportActionBar?.subtitle = null
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/detail/FutureDetailWeatherViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.detail
2 |
3 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
4 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
5 | import com.resocoder.forecastmvvm.internal.lazyDeferred
6 | import com.resocoder.forecastmvvm.ui.base.WeatherViewModel
7 | import org.threeten.bp.LocalDate
8 |
9 | class FutureDetailWeatherViewModel(
10 | private val detailDate: LocalDate,
11 | private val forecastRepository: ForecastRepository,
12 | unitProvider: UnitProvider
13 | ) : WeatherViewModel(forecastRepository, unitProvider) {
14 |
15 | val weather by lazyDeferred {
16 | forecastRepository.getFutureWeatherByDate(detailDate, super.isMetricUnit)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/list/FutureListWeatherViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.list
2 |
3 | import androidx.lifecycle.ViewModel;
4 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
5 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
6 | import com.resocoder.forecastmvvm.internal.lazyDeferred
7 | import com.resocoder.forecastmvvm.ui.base.WeatherViewModel
8 | import org.threeten.bp.LocalDate
9 |
10 | class FutureListWeatherViewModel(
11 | private val forecastRepository: ForecastRepository,
12 | unitProvider: UnitProvider
13 | ) : WeatherViewModel(forecastRepository, unitProvider) {
14 |
15 | val weatherEntries by lazyDeferred {
16 | forecastRepository.getFutureWeatherList(LocalDate.now(), super.isMetricUnit)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/base/WeatherViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
5 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
6 | import com.resocoder.forecastmvvm.internal.UnitSystem
7 | import com.resocoder.forecastmvvm.internal.lazyDeferred
8 |
9 |
10 | abstract class WeatherViewModel(
11 | private val forecastRepository: ForecastRepository,
12 | unitProvider: UnitProvider
13 | ) : ViewModel() {
14 |
15 | private val unitSystem = unitProvider.getUnitSystem()
16 |
17 | val isMetricUnit: Boolean
18 | get() = unitSystem == UnitSystem.METRIC
19 |
20 | val weatherLocation by lazyDeferred {
21 | forecastRepository.getWeatherLocation()
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/list/FutureListWeatherViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.list
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
6 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
7 |
8 |
9 | class FutureListWeatherViewModelFactory(
10 | private val forecastRepository: ForecastRepository,
11 | private val unitProvider: UnitProvider
12 | ) : ViewModelProvider.NewInstanceFactory() {
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | override fun create(modelClass: Class): T {
16 | return FutureListWeatherViewModel(
17 | forecastRepository,
18 | unitProvider
19 | ) as T
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/WeatherLocationDao.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.resocoder.forecastmvvm.data.db.entity.WEATHER_LOCATION_ID
9 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
10 |
11 |
12 | @Dao
13 | interface WeatherLocationDao {
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | fun upsert(weatherLocation: WeatherLocation)
17 |
18 | @Query("select * from weather_location where id = $WEATHER_LOCATION_ID")
19 | fun getLocation(): LiveData
20 |
21 | @Query("select * from weather_location where id = $WEATHER_LOCATION_ID")
22 | fun getLocationNonLive(): WeatherLocation?
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/detail/FutureDetailViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.detail
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
6 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
7 | import org.threeten.bp.LocalDate
8 |
9 |
10 | class FutureDetailWeatherViewModelFactory(
11 | private val detailDate: LocalDate,
12 | private val forecastRepository: ForecastRepository,
13 | private val unitProvider: UnitProvider
14 | ) : ViewModelProvider.NewInstanceFactory() {
15 |
16 | @Suppress("UNCHECKED_CAST")
17 | override fun create(modelClass: Class): T {
18 | return FutureDetailWeatherViewModel(detailDate, forecastRepository, unitProvider) as T
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_weather_sunny.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/current/MetricCurrentWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.current
2 |
3 | import androidx.room.ColumnInfo
4 |
5 |
6 | data class MetricCurrentWeatherEntry(
7 | @ColumnInfo(name = "tempC")
8 | override val temperature: Double,
9 | @ColumnInfo(name = "condition_text")
10 | override val conditionText: String,
11 | @ColumnInfo(name = "condition_icon")
12 | override val conditionIconUrl: String,
13 | @ColumnInfo(name = "windKph")
14 | override val windSpeed: Double,
15 | @ColumnInfo(name = "windDir")
16 | override val windDirection: String,
17 | @ColumnInfo(name = "precipMm")
18 | override val precipitationVolume: Double,
19 | @ColumnInfo(name = "feelslikeC")
20 | override val feelsLikeTemperature: Double,
21 | @ColumnInfo(name = "visKm")
22 | override val visibilityDistance: Double
23 | ) : UnitSpecificCurrentWeatherEntry
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/current/ImperialCurrentWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.current
2 |
3 | import androidx.room.ColumnInfo
4 |
5 |
6 | data class ImperialCurrentWeatherEntry(
7 | @ColumnInfo(name = "tempF")
8 | override val temperature: Double,
9 | @ColumnInfo(name = "condition_text")
10 | override val conditionText: String,
11 | @ColumnInfo(name = "condition_icon")
12 | override val conditionIconUrl: String,
13 | @ColumnInfo(name = "windMph")
14 | override val windSpeed: Double,
15 | @ColumnInfo(name = "windDir")
16 | override val windDirection: String,
17 | @ColumnInfo(name = "precipIn")
18 | override val precipitationVolume: Double,
19 | @ColumnInfo(name = "feelslikeF")
20 | override val feelsLikeTemperature: Double,
21 | @ColumnInfo(name = "visMiles")
22 | override val visibilityDistance: Double
23 | ) : UnitSpecificCurrentWeatherEntry
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/ConnectivityInterceptorImpl.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import com.resocoder.forecastmvvm.internal.NoConnectivityException
6 | import okhttp3.Interceptor
7 | import okhttp3.Response
8 | import java.io.IOException
9 |
10 | class ConnectivityInterceptorImpl(
11 | context: Context
12 | ) : ConnectivityInterceptor {
13 |
14 | private val appContext = context.applicationContext
15 |
16 | override fun intercept(chain: Interceptor.Chain): Response {
17 | if (!isOnline())
18 | throw NoConnectivityException()
19 | return chain.proceed(chain.request())
20 | }
21 |
22 | private fun isOnline(): Boolean {
23 | val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE)
24 | as ConnectivityManager
25 | val networkInfo = connectivityManager.activeNetworkInfo
26 | return networkInfo != null && networkInfo.isConnected
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/repository/ForecastRepository.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
5 | import com.resocoder.forecastmvvm.data.db.unitlocalized.current.UnitSpecificCurrentWeatherEntry
6 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail.UnitSpecificDetailFutureWeatherEntry
7 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.UnitSpecificSimpleFutureWeatherEntry
8 | import org.threeten.bp.LocalDate
9 |
10 |
11 | interface ForecastRepository {
12 | suspend fun getCurrentWeather(metric: Boolean): LiveData
13 |
14 | suspend fun getFutureWeatherList(startDate: LocalDate, metric: Boolean): LiveData>
15 |
16 | suspend fun getFutureWeatherByDate(date: LocalDate, metric: Boolean): LiveData
17 |
18 | suspend fun getWeatherLocation(): LiveData
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/CurrentWeatherDao.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.resocoder.forecastmvvm.data.db.entity.CURRENT_WEATHER_ID
9 | import com.resocoder.forecastmvvm.data.db.entity.CurrentWeatherEntry
10 | import com.resocoder.forecastmvvm.data.db.unitlocalized.current.ImperialCurrentWeatherEntry
11 | import com.resocoder.forecastmvvm.data.db.unitlocalized.current.MetricCurrentWeatherEntry
12 |
13 |
14 | @Dao
15 | interface CurrentWeatherDao {
16 | @Insert(onConflict = OnConflictStrategy.REPLACE)
17 | fun upsert(weatherEntry: CurrentWeatherEntry)
18 |
19 | @Query("select * from current_weather where id = $CURRENT_WEATHER_ID")
20 | fun getWeatherMetric(): LiveData
21 |
22 | @Query("select * from current_weather where id = $CURRENT_WEATHER_ID")
23 | fun getWeatherImperial(): LiveData
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/entity/WeatherLocation.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import com.google.gson.annotations.SerializedName
6 | import org.threeten.bp.Instant
7 | import org.threeten.bp.ZoneId
8 | import org.threeten.bp.ZonedDateTime
9 |
10 | const val WEATHER_LOCATION_ID = 0
11 |
12 | @Entity(tableName = "weather_location")
13 | data class WeatherLocation(
14 | val name: String,
15 | val region: String,
16 | val country: String,
17 | val lat: Double,
18 | val lon: Double,
19 | @SerializedName("tz_id")
20 | val tzId: String,
21 | @SerializedName("localtime_epoch")
22 | val localtimeEpoch: Long
23 | ) {
24 | @PrimaryKey(autoGenerate = false)
25 | var id: Int = WEATHER_LOCATION_ID
26 |
27 | val zonedDateTime: ZonedDateTime
28 | get() {
29 | val instant = Instant.ofEpochSecond(localtimeEpoch)
30 | val zoneId = ZoneId.of(tzId)
31 | return ZonedDateTime.ofInstant(instant, zoneId)
32 | }
33 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/detail/MetricDetailFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail
2 |
3 | import androidx.room.ColumnInfo
4 | import org.threeten.bp.LocalDate
5 |
6 |
7 | data class MetricDetailFutureWeatherEntry(
8 | @ColumnInfo(name = "date")
9 | override val date: LocalDate,
10 | @ColumnInfo(name = "maxtempC")
11 | override val maxTemperature: Double,
12 | @ColumnInfo(name = "mintempC")
13 | override val minTemperature: Double,
14 | @ColumnInfo(name = "avgtempC")
15 | override val avgTemperature: Double,
16 | @ColumnInfo(name = "condition_text")
17 | override val conditionText: String,
18 | @ColumnInfo(name = "condition_icon")
19 | override val conditionIconUrl: String,
20 | @ColumnInfo(name = "maxwindKph")
21 | override val maxWindSpeed: Double,
22 | @ColumnInfo(name = "totalprecipMm")
23 | override val totalPrecipitation: Double,
24 | @ColumnInfo(name = "avgvisKm")
25 | override val avgVisibilityDistance: Double,
26 | @ColumnInfo(name = "uv")
27 | override val uv: Double
28 | ) : UnitSpecificDetailFutureWeatherEntry
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/unitlocalized/future/detail/ImperialDetailFutureWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail
2 |
3 | import androidx.room.ColumnInfo
4 | import org.threeten.bp.LocalDate
5 |
6 |
7 | data class ImperialDetailFutureWeatherEntry(
8 | @ColumnInfo(name = "date")
9 | override val date: LocalDate,
10 | @ColumnInfo(name = "maxtempF")
11 | override val maxTemperature: Double,
12 | @ColumnInfo(name = "mintempF")
13 | override val minTemperature: Double,
14 | @ColumnInfo(name = "avgtempF")
15 | override val avgTemperature: Double,
16 | @ColumnInfo(name = "condition_text")
17 | override val conditionText: String,
18 | @ColumnInfo(name = "condition_icon")
19 | override val conditionIconUrl: String,
20 | @ColumnInfo(name = "maxwindMph")
21 | override val maxWindSpeed: Double,
22 | @ColumnInfo(name = "totalprecipIn")
23 | override val totalPrecipitation: Double,
24 | @ColumnInfo(name = "avgvisMiles")
25 | override val avgVisibilityDistance: Double,
26 | @ColumnInfo(name = "uv")
27 | override val uv: Double
28 | ) : UnitSpecificDetailFutureWeatherEntry
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/entity/Day.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.entity
2 |
3 | import androidx.room.Embedded
4 | import com.google.gson.annotations.SerializedName
5 | import com.resocoder.forecastmvvm.data.db.entity.Condition
6 |
7 | data class Day(
8 | @SerializedName("avgtemp_c")
9 | val avgtempC: Double,
10 | @SerializedName("avgtemp_f")
11 | val avgtempF: Double,
12 | @SerializedName("avgvis_km")
13 | val avgvisKm: Double,
14 | @SerializedName("avgvis_miles")
15 | val avgvisMiles: Double,
16 | @Embedded(prefix = "condition_")
17 | val condition: Condition,
18 | @SerializedName("maxtemp_c")
19 | val maxtempC: Double,
20 | @SerializedName("maxtemp_f")
21 | val maxtempF: Double,
22 | @SerializedName("maxwind_kph")
23 | val maxwindKph: Double,
24 | @SerializedName("maxwind_mph")
25 | val maxwindMph: Double,
26 | @SerializedName("mintemp_c")
27 | val mintempC: Double,
28 | @SerializedName("mintemp_f")
29 | val mintempF: Double,
30 | @SerializedName("totalprecip_in")
31 | val totalprecipIn: Double,
32 | @SerializedName("totalprecip_mm")
33 | val totalprecipMm: Double,
34 | val uv: Double
35 | )
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
11 |
12 |
18 |
19 |
20 |
22 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/entity/CurrentWeatherEntry.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db.entity
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import com.google.gson.annotations.SerializedName
7 |
8 | const val CURRENT_WEATHER_ID = 0
9 |
10 | @Entity(tableName = "current_weather")
11 | data class CurrentWeatherEntry(
12 | @SerializedName("temp_c")
13 | val tempC: Double,
14 | @SerializedName("temp_f")
15 | val tempF: Double,
16 | @SerializedName("is_day")
17 | val isDay: Int,
18 | @Embedded(prefix = "condition_")
19 | val condition: Condition,
20 | @SerializedName("wind_mph")
21 | val windMph: Double,
22 | @SerializedName("wind_kph")
23 | val windKph: Double,
24 | @SerializedName("wind_dir")
25 | val windDir: String,
26 | @SerializedName("precip_mm")
27 | val precipMm: Double,
28 | @SerializedName("precip_in")
29 | val precipIn: Double,
30 | @SerializedName("feelslike_c")
31 | val feelslikeC: Double,
32 | @SerializedName("feelslike_f")
33 | val feelslikeF: Double,
34 | @SerializedName("vis_km")
35 | val visKm: Double,
36 | @SerializedName("vis_miles")
37 | val visMiles: Double
38 | ) {
39 | @PrimaryKey(autoGenerate = false)
40 | var id: Int = CURRENT_WEATHER_ID
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
26 |
27 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/ForecastDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db
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.resocoder.forecastmvvm.data.db.entity.CurrentWeatherEntry
9 | import com.resocoder.forecastmvvm.data.db.entity.FutureWeatherEntry
10 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
11 |
12 |
13 | @Database(
14 | entities = [CurrentWeatherEntry::class, FutureWeatherEntry::class, WeatherLocation::class],
15 | version = 1
16 | )
17 | @TypeConverters(LocalDateConverter::class)
18 | abstract class ForecastDatabase : RoomDatabase() {
19 | abstract fun currentWeatherDao(): CurrentWeatherDao
20 | abstract fun futureWeatherDao(): FutureWeatherDao
21 | abstract fun weatherLocationDao(): WeatherLocationDao
22 |
23 | companion object {
24 | @Volatile private var instance: ForecastDatabase? = null
25 | private val LOCK = Any()
26 |
27 | operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
28 | instance ?: buildDatabase(context).also { instance = it }
29 | }
30 |
31 | private fun buildDatabase(context: Context) =
32 | Room.databaseBuilder(context.applicationContext,
33 | ForecastDatabase::class.java, "futureWeatherEntries.db")
34 | .build()
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/LifecycleBoundLocationManager.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleObserver
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.lifecycle.OnLifecycleEvent
8 | import com.google.android.gms.location.FusedLocationProviderClient
9 | import com.google.android.gms.location.LocationCallback
10 | import com.google.android.gms.location.LocationRequest
11 |
12 |
13 | class LifecycleBoundLocationManager(
14 | lifecycleOwner: LifecycleOwner,
15 | private val fusedLocationProviderClient: FusedLocationProviderClient,
16 | private val locationCallback: LocationCallback
17 | ) : LifecycleObserver {
18 |
19 | init {
20 | lifecycleOwner.lifecycle.addObserver(this)
21 | }
22 |
23 | private val locationRequest = LocationRequest().apply {
24 | interval = 5000
25 | fastestInterval = 5000
26 | priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
27 | }
28 |
29 | @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
30 | @SuppressLint("MissingPermission")
31 | fun startLocationUpdates() {
32 | fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null)
33 | }
34 |
35 | @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
36 | fun removeLocationUpdates() {
37 | fusedLocationProviderClient.removeLocationUpdates(locationCallback)
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/navigation/mobile_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
18 |
21 |
22 |
27 |
30 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/list/FutureWeatherItem.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.list
2 |
3 | import com.resocoder.forecastmvvm.R
4 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.MetricSimpleFutureWeatherEntry
5 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.UnitSpecificSimpleFutureWeatherEntry
6 | import com.resocoder.forecastmvvm.internal.glide.GlideApp
7 | import com.xwray.groupie.kotlinandroidextensions.Item
8 | import com.xwray.groupie.kotlinandroidextensions.ViewHolder
9 | import kotlinx.android.synthetic.main.item_future_weather.*
10 | import org.threeten.bp.format.DateTimeFormatter
11 | import org.threeten.bp.format.FormatStyle
12 |
13 |
14 | class FutureWeatherItem(
15 | val weatherEntry: UnitSpecificSimpleFutureWeatherEntry
16 | ) : Item() {
17 | override fun bind(viewHolder: ViewHolder, position: Int) {
18 | viewHolder.apply {
19 | textView_condition.text = weatherEntry.conditionText
20 | updateDate()
21 | updateTemperature()
22 | updateConditionImage()
23 | }
24 | }
25 |
26 | override fun getLayout() = R.layout.item_future_weather
27 |
28 | private fun ViewHolder.updateDate() {
29 | val dtFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
30 | textView_date.text = weatherEntry.date.format(dtFormatter)
31 | }
32 |
33 | private fun ViewHolder.updateTemperature() {
34 | val unitAbbreviation = if (weatherEntry is MetricSimpleFutureWeatherEntry) "°C"
35 | else "°F"
36 | textView_temperature.text = "${weatherEntry.avgTemperature}$unitAbbreviation"
37 | }
38 |
39 | private fun ViewHolder.updateConditionImage() {
40 | GlideApp.with(this.containerView)
41 | .load("http:" + weatherEntry.conditionIconUrl)
42 | .into(imageView_condition_icon)
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/db/FutureWeatherDao.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.resocoder.forecastmvvm.data.db.entity.FutureWeatherEntry
9 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail.ImperialDetailFutureWeatherEntry
10 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail.MetricDetailFutureWeatherEntry
11 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.ImperialSimpleFutureWeatherEntry
12 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.MetricSimpleFutureWeatherEntry
13 | import org.threeten.bp.LocalDate
14 |
15 | @Dao
16 | interface FutureWeatherDao {
17 | @Insert(onConflict = OnConflictStrategy.REPLACE)
18 | fun insert(futureWeatherEntries: List)
19 |
20 | @Query("select * from future_weather where date(date) >= date(:startDate)")
21 | fun getSimpleWeatherForecastsMetric(startDate: LocalDate): LiveData>
22 |
23 | @Query("select * from future_weather where date(date) >= date(:startDate)")
24 | fun getSimpleWeatherForecastsImperial(startDate: LocalDate): LiveData>
25 |
26 | @Query("select * from future_weather where date(date) = date(:date)")
27 | fun getDetailedWeatherByDateMetric(date: LocalDate): LiveData
28 |
29 | @Query("select * from future_weather where date(date) = date(:date)")
30 | fun getDetailedWeatherByDateImperial(date: LocalDate): LiveData
31 |
32 | @Query("select count(id) from future_weather where date(date) >= date(:startDate)")
33 | fun countFutureWeather(startDate: LocalDate): Int
34 |
35 | @Query("delete from future_weather where date(date) < date(:firstDateToKeep)")
36 | fun deleteOldEntries(firstDateToKeep: LocalDate)
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/WeatherNetworkDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.resocoder.forecastmvvm.data.network.response.CurrentWeatherResponse
7 | import com.resocoder.forecastmvvm.data.network.response.FutureWeatherResponse
8 | import com.resocoder.forecastmvvm.internal.NoConnectivityException
9 |
10 | const val FORECAST_DAYS_COUNT = 7
11 |
12 | class WeatherNetworkDataSourceImpl(
13 | private val apixuWeatherApiService: ApixuWeatherApiService
14 | ) : WeatherNetworkDataSource {
15 |
16 | private val _downloadedCurrentWeather = MutableLiveData()
17 | override val downloadedCurrentWeather: LiveData
18 | get() = _downloadedCurrentWeather
19 |
20 | override suspend fun fetchCurrentWeather(location: String, languageCode: String) {
21 | try {
22 | val fetchedCurrentWeather = apixuWeatherApiService
23 | .getCurrentWeather(location, languageCode)
24 | .await()
25 | _downloadedCurrentWeather.postValue(fetchedCurrentWeather)
26 | }
27 | catch (e: NoConnectivityException) {
28 | Log.e("Connectivity", "No internet connection.", e)
29 | }
30 | }
31 |
32 | private val _downloadedFutureWeather = MutableLiveData()
33 | override val downloadedFutureWeather: LiveData
34 | get() = _downloadedFutureWeather
35 |
36 | override suspend fun fetchFutureWeather(
37 | location: String,
38 | languageCode: String
39 | ) {
40 | try {
41 | val fetchedFutureWeather = apixuWeatherApiService
42 | .getFutureWeather(location, FORECAST_DAYS_COUNT, languageCode)
43 | .await()
44 | _downloadedFutureWeather.postValue(fetchedFutureWeather)
45 | }
46 | catch (e: NoConnectivityException) {
47 | Log.e("Connectivity", "No internet connection.", e)
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/future_list_weather_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
31 |
32 |
47 |
48 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/network/ApixuWeatherApiService.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.network
2 |
3 | import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
4 | import com.resocoder.forecastmvvm.data.network.response.CurrentWeatherResponse
5 | import com.resocoder.forecastmvvm.data.network.response.FutureWeatherResponse
6 | import kotlinx.coroutines.Deferred
7 | import okhttp3.Interceptor
8 | import okhttp3.OkHttpClient
9 | import retrofit2.Retrofit
10 | import retrofit2.converter.gson.GsonConverterFactory
11 | import retrofit2.http.GET
12 | import retrofit2.http.Query
13 |
14 |
15 | const val API_KEY = "89e8bd89085b41b7a4b142029180210"
16 |
17 | //http://api.apixu.com/v1/current.json?key=89e8bd89085b41b7a4b142029180210&q=London&lang=en
18 |
19 | interface ApixuWeatherApiService {
20 |
21 | @GET("current.json")
22 | fun getCurrentWeather(
23 | @Query("q") location: String,
24 | @Query("lang") languageCode: String = "en"
25 | ): Deferred
26 |
27 |
28 | // https://api.apixu.com/v1/forecast.json?key=89e8bd89085b41b7a4b142029180210&q=Los%20Angeles&days=1
29 | @GET("forecast.json")
30 | fun getFutureWeather(
31 | @Query("q") location: String,
32 | @Query("days") days: Int,
33 | @Query("lang") languageCode: String = "en"
34 | ): Deferred
35 |
36 | companion object {
37 | operator fun invoke(
38 | connectivityInterceptor: ConnectivityInterceptor
39 | ): ApixuWeatherApiService {
40 | val requestInterceptor = Interceptor { chain ->
41 |
42 | val url = chain.request()
43 | .url()
44 | .newBuilder()
45 | .addQueryParameter("key", API_KEY)
46 | .build()
47 | val request = chain.request()
48 | .newBuilder()
49 | .url(url)
50 | .build()
51 |
52 | return@Interceptor chain.proceed(request)
53 | }
54 |
55 | val okHttpClient = OkHttpClient.Builder()
56 | .addInterceptor(requestInterceptor)
57 | .addInterceptor(connectivityInterceptor)
58 | .build()
59 |
60 | return Retrofit.Builder()
61 | .client(okHttpClient)
62 | .baseUrl("https://api.apixu.com/v1/")
63 | .addCallAdapterFactory(CoroutineCallAdapterFactory())
64 | .addConverterFactory(GsonConverterFactory.create())
65 | .build()
66 | .create(ApixuWeatherApiService::class.java)
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ForecastApplication.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.preference.PreferenceManager
6 | import com.google.android.gms.location.LocationServices
7 | import com.jakewharton.threetenabp.AndroidThreeTen
8 | import com.resocoder.forecastmvvm.data.db.ForecastDatabase
9 | import com.resocoder.forecastmvvm.data.network.*
10 | import com.resocoder.forecastmvvm.data.provider.LocationProvider
11 | import com.resocoder.forecastmvvm.data.provider.LocationProviderImpl
12 | import com.resocoder.forecastmvvm.data.provider.UnitProvider
13 | import com.resocoder.forecastmvvm.data.provider.UnitProviderImpl
14 | import com.resocoder.forecastmvvm.data.repository.ForecastRepository
15 | import com.resocoder.forecastmvvm.data.repository.ForecastRepositoryImpl
16 | import com.resocoder.forecastmvvm.ui.weather.current.CurrentWeatherViewModelFactory
17 | import com.resocoder.forecastmvvm.ui.weather.future.detail.FutureDetailWeatherViewModelFactory
18 | import com.resocoder.forecastmvvm.ui.weather.future.list.FutureListWeatherViewModelFactory
19 | import org.kodein.di.Kodein
20 | import org.kodein.di.KodeinAware
21 | import org.kodein.di.android.x.androidXModule
22 | import org.kodein.di.generic.*
23 | import org.threeten.bp.LocalDate
24 |
25 |
26 | class ForecastApplication : Application(), KodeinAware {
27 | override val kodein = Kodein.lazy {
28 | import(androidXModule(this@ForecastApplication))
29 |
30 | bind() from singleton { ForecastDatabase(instance()) }
31 | bind() from singleton { instance().currentWeatherDao() }
32 | bind() from singleton { instance().futureWeatherDao() }
33 | bind() from singleton { instance().weatherLocationDao() }
34 | bind() with singleton { ConnectivityInterceptorImpl(instance()) }
35 | bind() from singleton { ApixuWeatherApiService(instance()) }
36 | bind() with singleton { WeatherNetworkDataSourceImpl(instance()) }
37 | bind() from provider { LocationServices.getFusedLocationProviderClient(instance()) }
38 | bind() with singleton { LocationProviderImpl(instance(), instance()) }
39 | bind() with singleton { ForecastRepositoryImpl(instance(), instance(), instance(), instance(), instance()) }
40 | bind() with singleton { UnitProviderImpl(instance()) }
41 | bind() from provider { CurrentWeatherViewModelFactory(instance(), instance()) }
42 | bind() from provider { FutureListWeatherViewModelFactory(instance(), instance()) }
43 | bind() from factory { detailDate: LocalDate -> FutureDetailWeatherViewModelFactory(detailDate, instance(), instance()) }
44 | }
45 |
46 | override fun onCreate() {
47 | super.onCreate()
48 | AndroidThreeTen.init(this)
49 | PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.os.Bundle
7 | import android.widget.Toast
8 | import androidx.core.app.ActivityCompat
9 | import androidx.core.content.ContextCompat
10 | import androidx.navigation.NavController
11 | import androidx.navigation.Navigation
12 | import androidx.navigation.ui.NavigationUI
13 | import androidx.navigation.ui.setupWithNavController
14 | import com.google.android.gms.location.FusedLocationProviderClient
15 | import com.google.android.gms.location.LocationCallback
16 | import com.google.android.gms.location.LocationResult
17 | import com.resocoder.forecastmvvm.R
18 | import kotlinx.android.synthetic.main.activity_main.*
19 | import org.kodein.di.KodeinAware
20 | import org.kodein.di.android.closestKodein
21 | import org.kodein.di.generic.instance
22 |
23 | private const val MY_PERMISSION_ACCESS_COARSE_LOCATION = 1
24 |
25 | class MainActivity : AppCompatActivity(), KodeinAware {
26 |
27 | override val kodein by closestKodein()
28 | private val fusedLocationProviderClient: FusedLocationProviderClient by instance()
29 |
30 | private val locationCallback = object : LocationCallback() {
31 | override fun onLocationResult(p0: LocationResult?) {
32 | super.onLocationResult(p0)
33 | }
34 | }
35 |
36 | private lateinit var navController: NavController
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | setContentView(R.layout.activity_main)
41 | setSupportActionBar(toolbar)
42 |
43 | navController = Navigation.findNavController(this, R.id.nav_host_fragment)
44 |
45 | bottom_nav.setupWithNavController(navController)
46 |
47 | NavigationUI.setupActionBarWithNavController(this, navController)
48 |
49 | requestLocationPermission()
50 |
51 | if (hasLocationPermission()) {
52 | bindLocationManager()
53 | }
54 | else
55 | requestLocationPermission()
56 | }
57 |
58 | private fun bindLocationManager() {
59 | LifecycleBoundLocationManager(
60 | this,
61 | fusedLocationProviderClient, locationCallback
62 | )
63 | }
64 |
65 | override fun onSupportNavigateUp(): Boolean {
66 | return NavigationUI.navigateUp(null, navController)
67 | }
68 |
69 | private fun requestLocationPermission() {
70 | ActivityCompat.requestPermissions(
71 | this,
72 | arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
73 | MY_PERMISSION_ACCESS_COARSE_LOCATION
74 | )
75 | }
76 |
77 | private fun hasLocationPermission(): Boolean {
78 | return ContextCompat.checkSelfPermission(this,
79 | Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
80 | }
81 |
82 | override fun onRequestPermissionsResult(
83 | requestCode: Int,
84 | permissions: Array,
85 | grantResults: IntArray
86 | ) {
87 | if (requestCode == MY_PERMISSION_ACCESS_COARSE_LOCATION) {
88 | if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
89 | bindLocationManager()
90 | else
91 | Toast.makeText(this, "Please, set location manually in settings", Toast.LENGTH_LONG).show()
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/provider/LocationProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.provider
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.content.Context
6 | import android.content.pm.PackageManager
7 | import android.location.Location
8 | import androidx.core.content.ContextCompat
9 | import com.google.android.gms.location.FusedLocationProviderClient
10 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
11 | import com.resocoder.forecastmvvm.internal.LocationPermissionNotGrantedException
12 | import com.resocoder.forecastmvvm.internal.asDeferred
13 | import kotlinx.coroutines.Deferred
14 |
15 | const val USE_DEVICE_LOCATION = "USE_DEVICE_LOCATION"
16 | const val CUSTOM_LOCATION = "CUSTOM_LOCATION"
17 |
18 | class LocationProviderImpl(
19 | private val fusedLocationProviderClient: FusedLocationProviderClient,
20 | context: Context
21 | ) : PreferenceProvider(context), LocationProvider {
22 |
23 | private val appContext = context.applicationContext
24 |
25 | override suspend fun hasLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
26 | val deviceLocationChanged = try {
27 | hasDeviceLocationChanged(lastWeatherLocation)
28 | } catch (e: LocationPermissionNotGrantedException) {
29 | false
30 | }
31 |
32 | return deviceLocationChanged || hasCustomLocationChanged(lastWeatherLocation)
33 | }
34 |
35 | override suspend fun getPreferredLocationString(): String {
36 | if (isUsingDeviceLocation()) {
37 | try {
38 | val deviceLocation = getLastDeviceLocation().await()
39 | ?: return "${getCustomLocationName()}"
40 | return "${deviceLocation.latitude},${deviceLocation.longitude}"
41 | } catch (e: LocationPermissionNotGrantedException) {
42 | return "${getCustomLocationName()}"
43 | }
44 | }
45 | else
46 | return "${getCustomLocationName()}"
47 | }
48 |
49 | private suspend fun hasDeviceLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
50 | if (!isUsingDeviceLocation())
51 | return false
52 |
53 | val deviceLocation = getLastDeviceLocation().await()
54 | ?: return false
55 |
56 | // Comparing doubles cannot be done with "=="
57 | val comparisonThreshold = 0.03
58 | return Math.abs(deviceLocation.latitude - lastWeatherLocation.lat) > comparisonThreshold &&
59 | Math.abs(deviceLocation.longitude - lastWeatherLocation.lon) > comparisonThreshold
60 | }
61 |
62 | private fun hasCustomLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
63 | if (!isUsingDeviceLocation()) {
64 | val customLocationName = getCustomLocationName()
65 | return customLocationName != lastWeatherLocation.name
66 | }
67 | return false
68 | }
69 |
70 | private fun isUsingDeviceLocation(): Boolean {
71 | return preferences.getBoolean(USE_DEVICE_LOCATION, true)
72 | }
73 |
74 | private fun getCustomLocationName(): String? {
75 | return preferences.getString(CUSTOM_LOCATION, null)
76 | }
77 |
78 | @SuppressLint("MissingPermission")
79 | private fun getLastDeviceLocation(): Deferred {
80 | return if (hasLocationPermission())
81 | fusedLocationProviderClient.lastLocation.asDeferred()
82 | else
83 | throw LocationPermissionNotGrantedException()
84 | }
85 |
86 | private fun hasLocationPermission(): Boolean {
87 | return ContextCompat.checkSelfPermission(appContext,
88 | Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
89 | }
90 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: "kotlin-kapt"
8 |
9 | apply plugin: 'androidx.navigation.safeargs'
10 |
11 | android {
12 | compileSdkVersion 28
13 | defaultConfig {
14 | applicationId "com.resocoder.forecastmvvm"
15 | minSdkVersion 21
16 | targetSdkVersion 28
17 | versionCode 1
18 | versionName "1.0"
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | }
28 |
29 | androidExtensions {
30 | experimental = true
31 | }
32 |
33 | dependencies {
34 | implementation fileTree(dir: 'libs', include: ['*.jar'])
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
36 | implementation 'androidx.appcompat:appcompat:1.0.0'
37 |
38 | // Navigation
39 | implementation "android.arch.navigation:navigation-fragment:$navigation_version"
40 | implementation "android.arch.navigation:navigation-ui:$navigation_version"
41 | implementation "android.arch.navigation:navigation-fragment-ktx:$navigation_version"
42 | implementation "android.arch.navigation:navigation-ui-ktx:$navigation_version"
43 |
44 | implementation "androidx.core:core-ktx:1.0.0"
45 | implementation "androidx.constraintlayout:constraintlayout:1.1.3"
46 |
47 | // Room
48 | implementation "androidx.room:room-runtime:$room_version"
49 | implementation "androidx.legacy:legacy-support-v4:1.0.0"
50 | implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
51 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
52 | kapt "androidx.room:room-compiler:$room_version"
53 |
54 | // Gson
55 | implementation "com.google.code.gson:gson:2.8.5"
56 |
57 | // Kotlin Android Coroutines
58 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
59 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1"
60 |
61 | // Retrofit
62 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
63 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
64 | implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
65 |
66 | // ViewModel
67 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
68 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
69 | kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
70 |
71 | // Kodein
72 | implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
73 | implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
74 |
75 | // Better dateTime-time support even on older Android versions
76 | implementation "com.jakewharton.threetenabp:threetenabp:1.1.0"
77 |
78 | // Glide
79 | implementation 'com.github.bumptech.glide:glide:4.8.0'
80 | kapt 'com.github.bumptech.glide:compiler:4.8.0'
81 |
82 | // Groupie RecyclerView
83 | implementation 'com.xwray:groupie:2.1.0'
84 | implementation 'com.xwray:groupie-kotlin-android-extensions:2.1.0'
85 |
86 | // Preference
87 | implementation "androidx.preference:preference:1.0.0"
88 |
89 | // WeatherLocation
90 | implementation "com.google.android.gms:play-services-location:16.0.0"
91 |
92 | // New Material Design
93 | implementation "com.google.android.material:material:1.0.0"
94 |
95 | testImplementation 'junit:junit:4.12'
96 | androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
97 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_future_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
32 |
33 |
48 |
49 |
66 |
67 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/list/FutureListWeatherFragment.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.list
2 |
3 | import androidx.lifecycle.ViewModelProviders
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.Toast
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.lifecycle.Observer
11 | import androidx.navigation.Navigation
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 |
14 | import com.resocoder.forecastmvvm.R
15 | import com.resocoder.forecastmvvm.data.db.LocalDateConverter
16 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.UnitSpecificSimpleFutureWeatherEntry
17 | import com.resocoder.forecastmvvm.ui.base.ScopedFragment
18 | import com.xwray.groupie.GroupAdapter
19 | import com.xwray.groupie.kotlinandroidextensions.ViewHolder
20 | import kotlinx.android.synthetic.main.future_list_weather_fragment.*
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.launch
23 | import org.kodein.di.KodeinAware
24 | import org.kodein.di.android.x.closestKodein
25 | import org.kodein.di.generic.instance
26 | import org.threeten.bp.LocalDate
27 |
28 | class FutureListWeatherFragment : ScopedFragment(), KodeinAware {
29 |
30 | override val kodein by closestKodein()
31 | private val viewModelFactory: FutureListWeatherViewModelFactory by instance()
32 |
33 | private lateinit var viewModel: FutureListWeatherViewModel
34 |
35 | override fun onCreateView(
36 | inflater: LayoutInflater, container: ViewGroup?,
37 | savedInstanceState: Bundle?
38 | ): View? {
39 | return inflater.inflate(R.layout.future_list_weather_fragment, container, false)
40 | }
41 |
42 | override fun onActivityCreated(savedInstanceState: Bundle?) {
43 | super.onActivityCreated(savedInstanceState)
44 | viewModel = ViewModelProviders.of(this, viewModelFactory)
45 | .get(FutureListWeatherViewModel::class.java)
46 | bindUI()
47 | }
48 |
49 | private fun bindUI() = launch(Dispatchers.Main) {
50 | val futureWeatherEntries = viewModel.weatherEntries.await()
51 | val weatherLocation = viewModel.weatherLocation.await()
52 |
53 | weatherLocation.observe(this@FutureListWeatherFragment, Observer { location ->
54 | if (location == null) return@Observer
55 | updateLocation(location.name)
56 | })
57 |
58 | futureWeatherEntries.observe(this@FutureListWeatherFragment, Observer { weatherEntries ->
59 | if (weatherEntries == null) return@Observer
60 |
61 | group_loading.visibility = View.GONE
62 |
63 | updateDateToNextWeek()
64 | initRecyclerView(weatherEntries.toFutureWeatherItems())
65 | })
66 | }
67 |
68 | private fun updateLocation(location: String) {
69 | (activity as? AppCompatActivity)?.supportActionBar?.title = location
70 | }
71 |
72 | private fun updateDateToNextWeek() {
73 | (activity as? AppCompatActivity)?.supportActionBar?.subtitle = "Next Week"
74 | }
75 |
76 | private fun List.toFutureWeatherItems() : List {
77 | return this.map {
78 | FutureWeatherItem(it)
79 | }
80 | }
81 |
82 | private fun initRecyclerView(items: List) {
83 | val groupAdapter = GroupAdapter().apply {
84 | addAll(items)
85 | }
86 |
87 | recyclerView.apply {
88 | layoutManager = LinearLayoutManager(this@FutureListWeatherFragment.context)
89 | adapter = groupAdapter
90 | }
91 |
92 | groupAdapter.setOnItemClickListener { item, view ->
93 | (item as? FutureWeatherItem)?.let {
94 | showWeatherDetail(it.weatherEntry.date, view)
95 | }
96 | }
97 | }
98 |
99 | private fun showWeatherDetail(date: LocalDate, view: View) {
100 | val dateString = LocalDateConverter.dateToString(date)!!
101 | val actionDetail = FutureListWeatherFragmentDirections.actionDetail(dateString)
102 | Navigation.findNavController(view).navigate(actionDetail)
103 | }
104 |
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/current/CurrentWeatherFragment.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.current
2 |
3 | import androidx.lifecycle.ViewModelProviders
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.lifecycle.Observer
11 |
12 | import com.resocoder.forecastmvvm.R
13 | import com.resocoder.forecastmvvm.data.network.ApixuWeatherApiService
14 | import com.resocoder.forecastmvvm.data.network.ConnectivityInterceptorImpl
15 | import com.resocoder.forecastmvvm.data.network.WeatherNetworkDataSourceImpl
16 | import com.resocoder.forecastmvvm.internal.glide.GlideApp
17 | import com.resocoder.forecastmvvm.ui.base.ScopedFragment
18 | import kotlinx.android.synthetic.main.current_weather_fragment.*
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.GlobalScope
21 | import kotlinx.coroutines.launch
22 | import org.kodein.di.Kodein
23 | import org.kodein.di.KodeinAware
24 | import org.kodein.di.android.x.closestKodein
25 | import org.kodein.di.generic.instance
26 |
27 | class CurrentWeatherFragment : ScopedFragment(), KodeinAware {
28 |
29 | override val kodein by closestKodein()
30 | private val viewModelFactory: CurrentWeatherViewModelFactory by instance()
31 |
32 | private lateinit var viewModel: CurrentWeatherViewModel
33 |
34 | override fun onCreateView(
35 | inflater: LayoutInflater, container: ViewGroup?,
36 | savedInstanceState: Bundle?
37 | ): View? {
38 | return inflater.inflate(R.layout.current_weather_fragment, container, false)
39 | }
40 |
41 | override fun onActivityCreated(savedInstanceState: Bundle?) {
42 | super.onActivityCreated(savedInstanceState)
43 |
44 | viewModel = ViewModelProviders.of(this, viewModelFactory)
45 | .get(CurrentWeatherViewModel::class.java)
46 |
47 | bindUI()
48 | }
49 |
50 | private fun bindUI() = launch {
51 | val currentWeather = viewModel.weather.await()
52 |
53 | val weatherLocation = viewModel.weatherLocation.await()
54 |
55 | weatherLocation.observe(this@CurrentWeatherFragment, Observer { location ->
56 | if (location == null) return@Observer
57 | updateLocation(location.name)
58 | })
59 |
60 | currentWeather.observe(this@CurrentWeatherFragment, Observer {
61 | if (it == null) return@Observer
62 |
63 | group_loading.visibility = View.GONE
64 | updateDateToToday()
65 | updateTemperatures(it.temperature, it.feelsLikeTemperature)
66 | updateCondition(it.conditionText)
67 | updatePrecipitation(it.precipitationVolume)
68 | updateWind(it.windDirection, it.windSpeed)
69 | updateVisibility(it.visibilityDistance)
70 |
71 | GlideApp.with(this@CurrentWeatherFragment)
72 | .load("http:${it.conditionIconUrl}")
73 | .into(imageView_condition_icon)
74 | })
75 | }
76 |
77 | private fun chooseLocalizedUnitAbbreviation(metric: String, imperial: String): String {
78 | return if (viewModel.isMetricUnit) metric else imperial
79 | }
80 |
81 | private fun updateLocation(location: String) {
82 | (activity as? AppCompatActivity)?.supportActionBar?.title = location
83 | }
84 |
85 | private fun updateDateToToday() {
86 | (activity as? AppCompatActivity)?.supportActionBar?.subtitle = "Today"
87 | }
88 |
89 | private fun updateTemperatures(temperature: Double, feelsLike: Double) {
90 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("°C", "°F")
91 | textView_temperature.text = "$temperature$unitAbbreviation"
92 | textView_feels_like_temperature.text = "Feels like $feelsLike$unitAbbreviation"
93 | }
94 |
95 | private fun updateCondition(condition: String) {
96 | textView_condition.text = condition
97 | }
98 |
99 | private fun updatePrecipitation(precipitationVolume: Double) {
100 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("mm", "in")
101 | textView_precipitation.text = "Preciptiation: $precipitationVolume $unitAbbreviation"
102 | }
103 |
104 | private fun updateWind(windDirection: String, windSpeed: Double) {
105 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("kph", "mph")
106 | textView_wind.text = "Wind: $windDirection, $windSpeed $unitAbbreviation"
107 | }
108 |
109 | private fun updateVisibility(visibilityDistance: Double) {
110 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("km", "mi.")
111 | textView_visibility.text = "Visibility: $visibilityDistance $unitAbbreviation"
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/ui/weather/future/detail/FutureDetailWeatherFragment.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.ui.weather.future.detail
2 |
3 | import androidx.lifecycle.ViewModelProviders
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.lifecycle.Observer
11 |
12 | import com.resocoder.forecastmvvm.R
13 | import com.resocoder.forecastmvvm.data.db.LocalDateConverter
14 | import com.resocoder.forecastmvvm.internal.DateNotFoundException
15 | import com.resocoder.forecastmvvm.internal.glide.GlideApp
16 | import com.resocoder.forecastmvvm.ui.base.ScopedFragment
17 | import kotlinx.android.synthetic.main.future_detail_weather_fragment.*
18 | import kotlinx.coroutines.Dispatchers
19 | import kotlinx.coroutines.launch
20 | import org.kodein.di.KodeinAware
21 | import org.kodein.di.android.x.closestKodein
22 | import org.kodein.di.generic.factory
23 | import org.threeten.bp.LocalDate
24 | import org.threeten.bp.format.DateTimeFormatter
25 | import org.threeten.bp.format.FormatStyle
26 |
27 | class FutureDetailWeatherFragment : ScopedFragment(), KodeinAware {
28 |
29 | override val kodein by closestKodein()
30 |
31 | private val viewModelFactoryInstanceFactory
32 | : ((LocalDate) -> FutureDetailWeatherViewModelFactory) by factory()
33 |
34 | private lateinit var viewModel: FutureDetailWeatherViewModel
35 |
36 | override fun onCreateView(
37 | inflater: LayoutInflater, container: ViewGroup?,
38 | savedInstanceState: Bundle?
39 | ): View? {
40 | return inflater.inflate(R.layout.future_detail_weather_fragment, container, false)
41 | }
42 |
43 | override fun onActivityCreated(savedInstanceState: Bundle?) {
44 | super.onActivityCreated(savedInstanceState)
45 |
46 | val safeArgs = arguments?.let { FutureDetailWeatherFragmentArgs.fromBundle(it) }
47 | val date = LocalDateConverter.stringToDate(safeArgs?.dateString) ?: throw DateNotFoundException()
48 |
49 | viewModel = ViewModelProviders.of(this, viewModelFactoryInstanceFactory(date))
50 | .get(FutureDetailWeatherViewModel::class.java)
51 |
52 | bindUI()
53 | }
54 |
55 | private fun bindUI() = launch(Dispatchers.Main) {
56 | val futureWeather = viewModel.weather.await()
57 | val weatherLocation = viewModel.weatherLocation.await()
58 |
59 | weatherLocation.observe(this@FutureDetailWeatherFragment, Observer { location ->
60 | if (location == null) return@Observer
61 | updateLocation(location.name)
62 | })
63 |
64 | futureWeather.observe(this@FutureDetailWeatherFragment, Observer { weatherEntry ->
65 | if (weatherEntry == null) return@Observer
66 |
67 | updateDate(weatherEntry.date)
68 | updateTemperatures(weatherEntry.avgTemperature,
69 | weatherEntry.minTemperature, weatherEntry.maxTemperature)
70 | updateCondition(weatherEntry.conditionText)
71 | updatePrecipitation(weatherEntry.totalPrecipitation)
72 | updateWindSpeed(weatherEntry.maxWindSpeed)
73 | updateVisibility(weatherEntry.avgVisibilityDistance)
74 | updateUv(weatherEntry.uv)
75 |
76 | GlideApp.with(this@FutureDetailWeatherFragment)
77 | .load("http:" + weatherEntry.conditionIconUrl)
78 | .into(imageView_condition_icon)
79 | })
80 | }
81 |
82 | private fun chooseLocalizedUnitAbbreviation(metric: String, imperial: String): String {
83 | return if (viewModel.isMetricUnit) metric else imperial
84 | }
85 |
86 | private fun updateLocation(location: String) {
87 | (activity as? AppCompatActivity)?.supportActionBar?.title = location
88 | }
89 |
90 | private fun updateDate(date: LocalDate) {
91 | (activity as? AppCompatActivity)?.supportActionBar?.subtitle =
92 | date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
93 | }
94 |
95 | private fun updateTemperatures(temperature: Double, min: Double, max: Double) {
96 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("°C", "°F")
97 | textView_temperature.text = "$temperature$unitAbbreviation"
98 | textView_min_max_temperature.text = "Min: $min$unitAbbreviation, Max: $max$unitAbbreviation"
99 | }
100 |
101 | private fun updateCondition(condition: String) {
102 | textView_condition.text = condition
103 | }
104 |
105 | private fun updatePrecipitation(precipitationVolume: Double) {
106 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("mm", "in")
107 | textView_precipitation.text = "Precipitation: $precipitationVolume $unitAbbreviation"
108 | }
109 |
110 | private fun updateWindSpeed(windSpeed: Double) {
111 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("kph", "mph")
112 | textView_wind.text = "Wind speed: $windSpeed $unitAbbreviation"
113 | }
114 |
115 | private fun updateVisibility(visibilityDistance: Double) {
116 | val unitAbbreviation = chooseLocalizedUnitAbbreviation("km", "mi.")
117 | textView_visibility.text = "Visibility: $visibilityDistance $unitAbbreviation"
118 | }
119 |
120 | private fun updateUv(uv: Double) {
121 | textView_uv.text = "UV: $uv"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/resocoder/forecastmvvm/data/repository/ForecastRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.resocoder.forecastmvvm.data.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.resocoder.forecastmvvm.data.db.CurrentWeatherDao
5 | import com.resocoder.forecastmvvm.data.db.FutureWeatherDao
6 | import com.resocoder.forecastmvvm.data.db.WeatherLocationDao
7 | import com.resocoder.forecastmvvm.data.db.entity.WeatherLocation
8 | import com.resocoder.forecastmvvm.data.db.unitlocalized.current.UnitSpecificCurrentWeatherEntry
9 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.detail.UnitSpecificDetailFutureWeatherEntry
10 | import com.resocoder.forecastmvvm.data.db.unitlocalized.future.list.UnitSpecificSimpleFutureWeatherEntry
11 | import com.resocoder.forecastmvvm.data.network.FORECAST_DAYS_COUNT
12 | import com.resocoder.forecastmvvm.data.network.WeatherNetworkDataSource
13 | import com.resocoder.forecastmvvm.data.network.response.CurrentWeatherResponse
14 | import com.resocoder.forecastmvvm.data.network.response.FutureWeatherResponse
15 | import com.resocoder.forecastmvvm.data.provider.LocationProvider
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.GlobalScope
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.withContext
20 | import org.threeten.bp.LocalDate
21 | import org.threeten.bp.ZonedDateTime
22 | import java.util.*
23 |
24 | class ForecastRepositoryImpl(
25 | private val currentWeatherDao: CurrentWeatherDao,
26 | private val futureWeatherDao: FutureWeatherDao,
27 | private val weatherLocationDao: WeatherLocationDao,
28 | private val weatherNetworkDataSource: WeatherNetworkDataSource,
29 | private val locationProvider: LocationProvider
30 | ) : ForecastRepository {
31 |
32 | init {
33 | weatherNetworkDataSource.apply {
34 | downloadedCurrentWeather.observeForever { newCurrentWeather ->
35 | persistFetchedCurrentWeather(newCurrentWeather)
36 | }
37 | downloadedFutureWeather.observeForever { newFutureWeather ->
38 | persistFetchedFutureWeather(newFutureWeather)
39 | }
40 | }
41 | }
42 |
43 | override suspend fun getCurrentWeather(metric: Boolean): LiveData {
44 | return withContext(Dispatchers.IO) {
45 | initWeatherData()
46 | return@withContext if (metric) currentWeatherDao.getWeatherMetric()
47 | else currentWeatherDao.getWeatherImperial()
48 | }
49 | }
50 |
51 | override suspend fun getFutureWeatherList(
52 | startDate: LocalDate,
53 | metric: Boolean
54 | ): LiveData> {
55 | return withContext(Dispatchers.IO) {
56 | initWeatherData()
57 | return@withContext if (metric) futureWeatherDao.getSimpleWeatherForecastsMetric(startDate)
58 | else futureWeatherDao.getSimpleWeatherForecastsImperial(startDate)
59 | }
60 | }
61 |
62 | override suspend fun getFutureWeatherByDate(
63 | date: LocalDate,
64 | metric: Boolean
65 | ): LiveData {
66 | return withContext(Dispatchers.IO) {
67 | initWeatherData()
68 | return@withContext if (metric) futureWeatherDao.getDetailedWeatherByDateMetric(date)
69 | else futureWeatherDao.getDetailedWeatherByDateImperial(date)
70 | }
71 | }
72 |
73 | override suspend fun getWeatherLocation(): LiveData {
74 | return withContext(Dispatchers.IO) {
75 | return@withContext weatherLocationDao.getLocation()
76 | }
77 | }
78 |
79 | private fun persistFetchedCurrentWeather(fetchedWeather: CurrentWeatherResponse) {
80 | GlobalScope.launch(Dispatchers.IO) {
81 | currentWeatherDao.upsert(fetchedWeather.currentWeatherEntry)
82 | weatherLocationDao.upsert(fetchedWeather.location)
83 | }
84 | }
85 |
86 | private fun persistFetchedFutureWeather(fetchedWeather: FutureWeatherResponse) {
87 |
88 | fun deleteOldForecastData() {
89 | val today = LocalDate.now()
90 | futureWeatherDao.deleteOldEntries(today)
91 | }
92 |
93 | GlobalScope.launch(Dispatchers.IO) {
94 | deleteOldForecastData()
95 | val futureWeatherList = fetchedWeather.futureWeatherEntries.entries
96 | futureWeatherDao.insert(futureWeatherList)
97 | weatherLocationDao.upsert(fetchedWeather.location)
98 | }
99 | }
100 |
101 | private suspend fun initWeatherData() {
102 | val lastWeatherLocation = weatherLocationDao.getLocationNonLive()
103 |
104 | if (lastWeatherLocation == null
105 | || locationProvider.hasLocationChanged(lastWeatherLocation)) {
106 | fetchCurrentWeather()
107 | fetchFutureWeather()
108 | return
109 | }
110 |
111 | if (isFetchCurrentNeeded(lastWeatherLocation.zonedDateTime))
112 | fetchCurrentWeather()
113 |
114 | if (isFetchFutureNeeded())
115 | fetchFutureWeather()
116 | }
117 |
118 | private suspend fun fetchCurrentWeather() {
119 | weatherNetworkDataSource.fetchCurrentWeather(
120 | locationProvider.getPreferredLocationString(),
121 | Locale.getDefault().language
122 | )
123 | }
124 |
125 | private suspend fun fetchFutureWeather() {
126 | weatherNetworkDataSource.fetchFutureWeather(
127 | locationProvider.getPreferredLocationString(),
128 | Locale.getDefault().language
129 | )
130 | }
131 |
132 | private fun isFetchCurrentNeeded(lastFetchTime: ZonedDateTime): Boolean {
133 | val thirtyMinutesAgo = ZonedDateTime.now().minusMinutes(30)
134 | return lastFetchTime.isBefore(thirtyMinutesAgo)
135 | }
136 |
137 | private fun isFetchFutureNeeded(): Boolean {
138 | val today = LocalDate.now()
139 | val futureWeatherCount = futureWeatherDao.countFutureWeather(today)
140 | return futureWeatherCount < FORECAST_DAYS_COUNT
141 | }
142 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/future_detail_weather_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
30 |
31 |
49 |
50 |
64 |
65 |
81 |
82 |
98 |
99 |
115 |
116 |
132 |
133 |
149 |
150 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/current_weather_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
24 |
25 |
38 |
39 |
54 |
55 |
71 |
72 |
90 |
91 |
106 |
107 |
121 |
122 |
138 |
139 |
155 |
156 |
172 |
173 |
--------------------------------------------------------------------------------