From ae6fff219ffc8af7ef29b7bc5c80debaaf43abcd Mon Sep 17 00:00:00 2001 From: sepehr99p Date: Mon, 13 Nov 2023 11:39:41 +0330 Subject: [PATCH 1/3] refactor : use case added for clean architecture best practice used flow on api calls error handling and retry policy added Loading state added to Resource --- .../weatherapp/data/remote/WeatherApi.kt | 3 +- .../data/repository/WeatherRepositoryImpl.kt | 19 ++----- .../com/plcoding/weatherapp/di/AppModule.kt | 8 +++ .../domain/repository/WeatherRepository.kt | 3 +- .../domain/usecase/GetWeatherUseCase.kt | 24 ++++++++ .../plcoding/weatherapp/domain/util/Error.kt | 26 +++++++++ .../weatherapp/domain/util/Resource.kt | 27 ++++++++- .../weatherapp/domain/util/RetryPolicy.kt | 35 ++++++++++++ .../presentation/WeatherViewModel.kt | 55 +++++++++++++------ 9 files changed, 164 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetWeatherUseCase.kt create mode 100644 app/src/main/java/com/plcoding/weatherapp/domain/util/Error.kt create mode 100644 app/src/main/java/com/plcoding/weatherapp/domain/util/RetryPolicy.kt diff --git a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt b/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt index 67ac577..4ad0aa6 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt @@ -1,5 +1,6 @@ package com.plcoding.weatherapp.data.remote +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query @@ -9,5 +10,5 @@ interface WeatherApi { suspend fun getWeatherData( @Query("latitude") lat: Double, @Query("longitude") long: Double - ): WeatherDto + ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt b/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt index 49879dd..9ab67da 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt @@ -1,27 +1,16 @@ package com.plcoding.weatherapp.data.repository -import com.plcoding.weatherapp.data.mappers.toWeatherInfo import com.plcoding.weatherapp.data.remote.WeatherApi import com.plcoding.weatherapp.domain.repository.WeatherRepository import com.plcoding.weatherapp.domain.util.Resource +import com.plcoding.weatherapp.domain.util.checkResponse import com.plcoding.weatherapp.domain.weather.WeatherInfo import javax.inject.Inject class WeatherRepositoryImpl @Inject constructor( private val api: WeatherApi -): WeatherRepository { +) : WeatherRepository { - override suspend fun getWeatherData(lat: Double, long: Double): Resource { - return try { - Resource.Success( - data = api.getWeatherData( - lat = lat, - long = long - ).toWeatherInfo() - ) - } catch(e: Exception) { - e.printStackTrace() - Resource.Error(e.message ?: "An unknown error occurred.") - } - } + override suspend fun getWeatherData(lat: Double, long: Double): Resource = + checkResponse(api.getWeatherData(lat, long)) } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt b/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt index 6662377..0a24d30 100644 --- a/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt +++ b/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt @@ -4,6 +4,8 @@ import android.app.Application import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.plcoding.weatherapp.data.remote.WeatherApi +import com.plcoding.weatherapp.domain.repository.WeatherRepository +import com.plcoding.weatherapp.domain.usecase.GetWeatherUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -32,4 +34,10 @@ object AppModule { fun provideFusedLocationProviderClient(app: Application): FusedLocationProviderClient { return LocationServices.getFusedLocationProviderClient(app) } + + @Provides + fun provideGetWeatherUseCase(weatherRepository: WeatherRepository): GetWeatherUseCase { + return GetWeatherUseCase(weatherRepository) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt b/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt index ade5766..56f4d6e 100644 --- a/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt +++ b/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt @@ -1,7 +1,8 @@ package com.plcoding.weatherapp.domain.repository - +import com.plcoding.weatherapp.data.remote.WeatherDto import com.plcoding.weatherapp.domain.util.Resource import com.plcoding.weatherapp.domain.weather.WeatherInfo +import retrofit2.Response interface WeatherRepository { suspend fun getWeatherData(lat: Double, long: Double): Resource diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetWeatherUseCase.kt b/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetWeatherUseCase.kt new file mode 100644 index 0000000..f9ad95b --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetWeatherUseCase.kt @@ -0,0 +1,24 @@ +package com.plcoding.weatherapp.domain.usecase + +import com.plcoding.weatherapp.domain.repository.WeatherRepository +import com.plcoding.weatherapp.domain.util.DefaultRetryPolicy +import com.plcoding.weatherapp.domain.util.Resource +import com.plcoding.weatherapp.domain.util.checkError +import com.plcoding.weatherapp.domain.util.retryWithPolicy +import com.plcoding.weatherapp.domain.weather.WeatherInfo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onStart + +class GetWeatherUseCase constructor( + private val weatherRepository: WeatherRepository +) { + + suspend operator fun invoke(lat : Double, long : Double): Flow> = flow { + emit(weatherRepository.getWeatherData(lat, long)) + }.retryWithPolicy(DefaultRetryPolicy()) + .catch { emit(checkError(it)) } + .onStart { emit(Resource.Loading()) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/util/Error.kt b/app/src/main/java/com/plcoding/weatherapp/domain/util/Error.kt new file mode 100644 index 0000000..d0c7b6c --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/domain/util/Error.kt @@ -0,0 +1,26 @@ +package com.plcoding.weatherapp.domain.util + +import java.net.ProtocolException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException + +fun checkError(throwable: Throwable): Resource.Error { + return when (throwable) { + is UnknownHostException -> { + Resource.Error("No internet Connection") + } + is SSLHandshakeException -> { + Resource.Error("SSL handshake error") + } + is SocketTimeoutException -> { + Resource.Error(throwable.localizedMessage ?: "Socket Timeout") + } + is ProtocolException -> { + Resource.Error(throwable.localizedMessage ?: "Protocol Exception") + } + else -> { + Resource.Error(throwable.localizedMessage ?: "Error") + } + } +} diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt b/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt index 21dae92..a206453 100644 --- a/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt +++ b/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt @@ -1,6 +1,29 @@ package com.plcoding.weatherapp.domain.util +import com.plcoding.weatherapp.data.mappers.toWeatherInfo +import com.plcoding.weatherapp.data.remote.WeatherDto +import com.plcoding.weatherapp.domain.weather.WeatherInfo +import retrofit2.Response + sealed class Resource(val data: T? = null, val message: String? = null) { - class Success(data: T?): Resource(data) - class Error(message: String, data: T? = null): Resource(data, message) + class Success(data: T?) : Resource(data) + class Error(message: String, data: T? = null) : Resource(data, message) + class Loading(data: T? = null) : Resource(data) +} + +fun checkResponse(response: Response): Resource { + return if (response.isSuccessful) { + response.body()?.let { + Resource.Success(it.toWeatherInfo()) + } ?: run { + Resource.Error("Empty body") + } + } else { + if (response.code().toString().startsWith("5")) { + Resource.Error("Server error") + } else { + Resource.Error(response.errorBody()?.string() ?: "Default Error") + } + } } + diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/util/RetryPolicy.kt b/app/src/main/java/com/plcoding/weatherapp/domain/util/RetryPolicy.kt new file mode 100644 index 0000000..bc90d1d --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/domain/util/RetryPolicy.kt @@ -0,0 +1,35 @@ +package com.plcoding.weatherapp.domain.util + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.retryWhen +import java.io.IOException + + +interface RetryPolicy { + val numRetries: Long + val delayMillis: Long + val delayFactor: Long +} + +data class DefaultRetryPolicy( + override val numRetries: Long = 3, + override val delayMillis: Long = 400, + override val delayFactor: Long = 1 +) : RetryPolicy + +fun Flow.retryWithPolicy( + retryPolicy: RetryPolicy +): Flow { + var currentDelay = retryPolicy.delayMillis + val delayFactor = retryPolicy.delayFactor + return retryWhen { cause, attempt -> + if (cause is IOException && attempt < retryPolicy.numRetries) { + delay(currentDelay) + currentDelay *= delayFactor + return@retryWhen true + } else { + return@retryWhen false + } + } +} diff --git a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt index e35faee..99ea601 100644 --- a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt +++ b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt @@ -1,22 +1,25 @@ package com.plcoding.weatherapp.presentation +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.plcoding.weatherapp.domain.location.LocationTracker -import com.plcoding.weatherapp.domain.repository.WeatherRepository +import com.plcoding.weatherapp.domain.usecase.GetWeatherUseCase import com.plcoding.weatherapp.domain.util.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class WeatherViewModel @Inject constructor( - private val repository: WeatherRepository, + private val getWeatherUseCase: GetWeatherUseCase, private val locationTracker: LocationTracker -): ViewModel() { +) : ViewModel() { var state by mutableStateOf(WeatherState()) private set @@ -28,20 +31,38 @@ class WeatherViewModel @Inject constructor( error = null ) locationTracker.getCurrentLocation()?.let { location -> - when(val result = repository.getWeatherData(location.latitude, location.longitude)) { - is Resource.Success -> { - state = state.copy( - weatherInfo = result.data, - isLoading = false, - error = null - ) - } - is Resource.Error -> { - state = state.copy( - weatherInfo = null, - isLoading = false, - error = result.message - ) + getWeatherUseCase.invoke(location.latitude, location.longitude).catch { + Log.e("WeatherViewModel", "loadWeatherInfo: ", it) + state = state.copy( + weatherInfo = null, + isLoading = false, + error = it.localizedMessage + ) + }.collect { + when (it) { + is Resource.Success -> { + state = state.copy( + weatherInfo = it.data, + isLoading = false, + error = null + ) + } + + is Resource.Error -> { + state = state.copy( + weatherInfo = null, + isLoading = false, + error = it.message + ) + } + + is Resource.Loading -> { + state = state.copy( + weatherInfo = null, + isLoading = true, + error = null + ) + } } } } ?: kotlin.run { From d66f12049c03fc43a14e077945139de0eeb48af5 Mon Sep 17 00:00:00 2001 From: sepehr99p Date: Tue, 14 Nov 2023 16:20:05 +0330 Subject: [PATCH 2/3] show hourly forecast based on current time --- .idea/inspectionProfiles/Project_Default.xml | 32 +++++++++++++++++++ .../presentation/WeatherForecast.kt | 22 +++++++++---- 2 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..103e00c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt index a75d391..0685f6d 100644 --- a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt +++ b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt @@ -1,5 +1,6 @@ package com.plcoding.weatherapp.presentation +import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -9,6 +10,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import java.sql.Timestamp +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter @Composable fun WeatherForecast( @@ -29,12 +34,17 @@ fun WeatherForecast( Spacer(modifier = Modifier.height(16.dp)) LazyRow(content = { items(data) { weatherData -> - HourlyWeatherDisplay( - weatherData = weatherData, - modifier = Modifier - .height(100.dp) - .padding(horizontal = 16.dp) - ) + val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm").parse(weatherData.time.toString()) + date?.let { + if (it.time > System.currentTimeMillis()) { + HourlyWeatherDisplay( + weatherData = weatherData, + modifier = Modifier + .height(100.dp) + .padding(horizontal = 16.dp) + ) + } + } } }) } From a56c9b9312100c7de9a74d927c7a72f4623bfb65 Mon Sep 17 00:00:00 2001 From: sepehr99p Date: Tue, 14 Nov 2023 18:02:17 +0330 Subject: [PATCH 3/3] daily forecast backend implemented --- .../weatherapp/data/dto/ForecastDataDto.kt | 26 ++++++++++++++ .../weatherapp/data/dto/ForecastDto.kt | 9 +++++ .../data/{remote => dto}/WeatherDataDto.kt | 2 +- .../data/{remote => dto}/WeatherDto.kt | 2 +- .../weatherapp/data/mappers/WeatherMappers.kt | 10 ++++-- .../weatherapp/data/remote/WeatherApi.kt | 9 +++++ .../data/repository/WeatherRepositoryImpl.kt | 6 ++++ .../com/plcoding/weatherapp/di/AppModule.kt | 36 ++++++++++++++++--- .../domain/repository/WeatherRepository.kt | 5 +-- .../domain/usecase/GetForecastUseCase.kt | 25 +++++++++++++ .../weatherapp/domain/util/Resource.kt | 23 +++++++++++- .../weatherapp/presentation/MainActivity.kt | 1 + .../presentation/WeatherForecast.kt | 10 +++--- .../presentation/WeatherViewModel.kt | 25 +++++++++++++ 14 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDataDto.kt create mode 100644 app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDto.kt rename app/src/main/java/com/plcoding/weatherapp/data/{remote => dto}/WeatherDataDto.kt (91%) rename app/src/main/java/com/plcoding/weatherapp/data/{remote => dto}/WeatherDto.kt (74%) create mode 100644 app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetForecastUseCase.kt diff --git a/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDataDto.kt b/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDataDto.kt new file mode 100644 index 0000000..878086b --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDataDto.kt @@ -0,0 +1,26 @@ +package com.plcoding.weatherapp.data.dto + +import com.squareup.moshi.Json +import java.time.LocalDateTime +import java.util.Date + +data class ForecastDataDto ( + val time: List, + @field:Json(name = "temperature_2m_max") + val maxTemperatures: List, + @field:Json(name = "temperature_2m_min") + val minTemperatures : List, + @field:Json(name = "weathercode") + val weatherCodes: List, + @field:Json(name = "sunrise") + val sunrise: List, + @field:Json(name = "sunset") + val sunset: List, + @field:Json(name = "rain_sum") + val rainSum: List, + @field:Json(name = "showers_sum") + val showersSum: List, + @field:Json(name = "snowfall_sum") + val snowfallSum: List + +) \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDto.kt b/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDto.kt new file mode 100644 index 0000000..485beee --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/data/dto/ForecastDto.kt @@ -0,0 +1,9 @@ +package com.plcoding.weatherapp.data.dto + +import com.squareup.moshi.Json + +data class ForecastDto ( + @field:Json(name = "daily") + val forecastDataDto: ForecastDataDto + +) \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDataDto.kt b/app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDataDto.kt similarity index 91% rename from app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDataDto.kt rename to app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDataDto.kt index aa25dfc..351a932 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDataDto.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDataDto.kt @@ -1,4 +1,4 @@ -package com.plcoding.weatherapp.data.remote +package com.plcoding.weatherapp.data.dto import com.squareup.moshi.Json diff --git a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDto.kt b/app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDto.kt similarity index 74% rename from app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDto.kt rename to app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDto.kt index e647e50..ff0c7ec 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherDto.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/dto/WeatherDto.kt @@ -1,4 +1,4 @@ -package com.plcoding.weatherapp.data.remote +package com.plcoding.weatherapp.data.dto import com.squareup.moshi.Json diff --git a/app/src/main/java/com/plcoding/weatherapp/data/mappers/WeatherMappers.kt b/app/src/main/java/com/plcoding/weatherapp/data/mappers/WeatherMappers.kt index cc18e49..5ef4a9e 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/mappers/WeatherMappers.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/mappers/WeatherMappers.kt @@ -1,7 +1,9 @@ package com.plcoding.weatherapp.data.mappers -import com.plcoding.weatherapp.data.remote.WeatherDataDto -import com.plcoding.weatherapp.data.remote.WeatherDto +import com.plcoding.weatherapp.data.dto.ForecastDataDto +import com.plcoding.weatherapp.data.dto.ForecastDto +import com.plcoding.weatherapp.data.dto.WeatherDataDto +import com.plcoding.weatherapp.data.dto.WeatherDto import com.plcoding.weatherapp.domain.weather.WeatherData import com.plcoding.weatherapp.domain.weather.WeatherInfo import com.plcoding.weatherapp.domain.weather.WeatherType @@ -49,4 +51,8 @@ fun WeatherDto.toWeatherInfo(): WeatherInfo { weatherDataPerDay = weatherDataMap, currentWeatherData = currentWeatherData ) +} + +fun ForecastDto.toForecastInfo() : ForecastDataDto { + return this.forecastDataDto } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt b/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt index 4ad0aa6..06683c8 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/remote/WeatherApi.kt @@ -1,5 +1,7 @@ package com.plcoding.weatherapp.data.remote +import com.plcoding.weatherapp.data.dto.ForecastDto +import com.plcoding.weatherapp.data.dto.WeatherDto import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query @@ -11,4 +13,11 @@ interface WeatherApi { @Query("latitude") lat: Double, @Query("longitude") long: Double ): Response + + @GET("v1/forecast?daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,rain_sum,showers_sum,snowfall_sum") + suspend fun getDailyForecast( + @Query("latitude") lat: Double, + @Query("longitude") long: Double + ) : Response + } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt b/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt index 9ab67da..ea39086 100644 --- a/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt +++ b/app/src/main/java/com/plcoding/weatherapp/data/repository/WeatherRepositoryImpl.kt @@ -1,9 +1,12 @@ package com.plcoding.weatherapp.data.repository +import com.plcoding.weatherapp.data.dto.ForecastDataDto +import com.plcoding.weatherapp.data.dto.ForecastDto import com.plcoding.weatherapp.data.remote.WeatherApi import com.plcoding.weatherapp.domain.repository.WeatherRepository import com.plcoding.weatherapp.domain.util.Resource import com.plcoding.weatherapp.domain.util.checkResponse +import com.plcoding.weatherapp.domain.util.checkResponse2 import com.plcoding.weatherapp.domain.weather.WeatherInfo import javax.inject.Inject @@ -13,4 +16,7 @@ class WeatherRepositoryImpl @Inject constructor( override suspend fun getWeatherData(lat: Double, long: Double): Resource = checkResponse(api.getWeatherData(lat, long)) + + override suspend fun getForecast(lat: Double, long: Double): Resource = + checkResponse2(api.getDailyForecast(lat,long)) } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt b/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt index 0a24d30..ac35e49 100644 --- a/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt +++ b/app/src/main/java/com/plcoding/weatherapp/di/AppModule.kt @@ -5,14 +5,18 @@ import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.plcoding.weatherapp.data.remote.WeatherApi import com.plcoding.weatherapp.domain.repository.WeatherRepository +import com.plcoding.weatherapp.domain.usecase.GetForecastUseCase import com.plcoding.weatherapp.domain.usecase.GetWeatherUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.create +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @@ -21,14 +25,35 @@ object AppModule { @Provides @Singleton - fun provideWeatherApi(): WeatherApi { + fun provideWeatherApi( + httpClient: OkHttpClient.Builder + ): WeatherApi { return Retrofit.Builder() .baseUrl("https://api.open-meteo.com/") .addConverterFactory(MoshiConverterFactory.create()) + .client(httpClient.build()) .build() .create() } + @Provides + @Singleton + fun provideOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient.Builder { + return OkHttpClient.Builder() + .addInterceptor(httpLoggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) // Adjust as needed + .readTimeout(30, TimeUnit.SECONDS) // Adjust as needed + .retryOnConnectionFailure(true) + } + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + } + @Provides @Singleton fun provideFusedLocationProviderClient(app: Application): FusedLocationProviderClient { @@ -36,8 +61,11 @@ object AppModule { } @Provides - fun provideGetWeatherUseCase(weatherRepository: WeatherRepository): GetWeatherUseCase { - return GetWeatherUseCase(weatherRepository) - } + fun provideGetWeatherUseCase(weatherRepository: WeatherRepository): GetWeatherUseCase = + GetWeatherUseCase(weatherRepository) + + @Provides + fun provideGetForecastWeatherUseCase(weatherRepository: WeatherRepository) : GetForecastUseCase = + GetForecastUseCase(weatherRepository) } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt b/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt index 56f4d6e..589bc21 100644 --- a/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt +++ b/app/src/main/java/com/plcoding/weatherapp/domain/repository/WeatherRepository.kt @@ -1,9 +1,10 @@ package com.plcoding.weatherapp.domain.repository -import com.plcoding.weatherapp.data.remote.WeatherDto +import com.plcoding.weatherapp.data.dto.ForecastDataDto +import com.plcoding.weatherapp.data.dto.ForecastDto import com.plcoding.weatherapp.domain.util.Resource import com.plcoding.weatherapp.domain.weather.WeatherInfo -import retrofit2.Response interface WeatherRepository { suspend fun getWeatherData(lat: Double, long: Double): Resource + suspend fun getForecast(lat: Double, long: Double): Resource } \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetForecastUseCase.kt b/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetForecastUseCase.kt new file mode 100644 index 0000000..28b8b90 --- /dev/null +++ b/app/src/main/java/com/plcoding/weatherapp/domain/usecase/GetForecastUseCase.kt @@ -0,0 +1,25 @@ +package com.plcoding.weatherapp.domain.usecase + +import com.plcoding.weatherapp.data.dto.ForecastDataDto +import com.plcoding.weatherapp.data.dto.ForecastDto +import com.plcoding.weatherapp.domain.repository.WeatherRepository +import com.plcoding.weatherapp.domain.util.DefaultRetryPolicy +import com.plcoding.weatherapp.domain.util.Resource +import com.plcoding.weatherapp.domain.util.checkError +import com.plcoding.weatherapp.domain.util.retryWithPolicy +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onStart + +class GetForecastUseCase constructor( + private val weatherRepository: WeatherRepository +) { + + suspend operator fun invoke(lat : Double, long : Double): Flow> = flow { + emit(weatherRepository.getForecast(lat, long)) + }.retryWithPolicy(DefaultRetryPolicy()) + .catch { emit(checkError(it)) } + .onStart { emit(Resource.Loading()) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt b/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt index a206453..ccee1a2 100644 --- a/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt +++ b/app/src/main/java/com/plcoding/weatherapp/domain/util/Resource.kt @@ -1,7 +1,11 @@ package com.plcoding.weatherapp.domain.util +import android.util.Log +import com.plcoding.weatherapp.data.dto.ForecastDataDto +import com.plcoding.weatherapp.data.dto.ForecastDto import com.plcoding.weatherapp.data.mappers.toWeatherInfo -import com.plcoding.weatherapp.data.remote.WeatherDto +import com.plcoding.weatherapp.data.dto.WeatherDto +import com.plcoding.weatherapp.data.mappers.toForecastInfo import com.plcoding.weatherapp.domain.weather.WeatherInfo import retrofit2.Response @@ -27,3 +31,20 @@ fun checkResponse(response: Response): Resource { } } +fun checkResponse2(response: Response): Resource { + Log.i("TAG", "checkResponse2: ") + return if (response.isSuccessful) { + response.body()?.let { + Resource.Success(it) + } ?: run { + Resource.Error("Empty body") + } + } else { + if (response.code().toString().startsWith("5")) { + Resource.Error("Server error") + } else { + Resource.Error(response.errorBody()?.string() ?: "Default Error") + } + } +} + diff --git a/app/src/main/java/com/plcoding/weatherapp/presentation/MainActivity.kt b/app/src/main/java/com/plcoding/weatherapp/presentation/MainActivity.kt index 3445490..8595e4f 100644 --- a/app/src/main/java/com/plcoding/weatherapp/presentation/MainActivity.kt +++ b/app/src/main/java/com/plcoding/weatherapp/presentation/MainActivity.kt @@ -33,6 +33,7 @@ class MainActivity : ComponentActivity() { ActivityResultContracts.RequestMultiplePermissions() ) { viewModel.loadWeatherInfo() + viewModel.loadForecast() } permissionLauncher.launch(arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, diff --git a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt index 0685f6d..2793cd7 100644 --- a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt +++ b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherForecast.kt @@ -1,7 +1,10 @@ package com.plcoding.weatherapp.presentation -import android.util.Log -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.Text @@ -10,10 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import java.sql.Timestamp -import java.text.DateFormat import java.text.SimpleDateFormat -import java.time.format.DateTimeFormatter @Composable fun WeatherForecast( diff --git a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt index 99ea601..5a3c124 100644 --- a/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt +++ b/app/src/main/java/com/plcoding/weatherapp/presentation/WeatherViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.plcoding.weatherapp.domain.location.LocationTracker +import com.plcoding.weatherapp.domain.usecase.GetForecastUseCase import com.plcoding.weatherapp.domain.usecase.GetWeatherUseCase import com.plcoding.weatherapp.domain.util.Resource import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,12 +19,36 @@ import javax.inject.Inject @HiltViewModel class WeatherViewModel @Inject constructor( private val getWeatherUseCase: GetWeatherUseCase, + private val getForecastUseCase: GetForecastUseCase, private val locationTracker: LocationTracker ) : ViewModel() { + private val TAG = "WeatherViewModel" var state by mutableStateOf(WeatherState()) private set + fun loadForecast() { + viewModelScope.launch { + locationTracker.getCurrentLocation()?.let { location -> + getForecastUseCase.invoke(location.latitude,location.longitude).catch { + Log.e(TAG, "loadForecast: ", it) + }.collect { + when(it) { + is Resource.Success -> { + Log.i(TAG, "loadForecast: successful") + } + is Resource.Loading -> { + Log.i(TAG, "loadForecast: loading") + } + is Resource.Error -> { + Log.i(TAG, "loadForecast: error") + } + } + } + } + } + } + fun loadWeatherInfo() { viewModelScope.launch { state = state.copy(