From dc1d3328cb4b8f6ed8aed48874284cebdcb4fcd7 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 11 Apr 2023 10:23:00 -0400 Subject: [PATCH] many changes including expandable days, also bump dependencies --- app/build.gradle.kts | 39 ++++++++------ .../com/henryhiles/qweather/di/AppModule.kt | 23 ++++---- .../domain/location/LocationTracker.kt | 23 +------- .../qweather/domain/mappers/WeatherMappers.kt | 26 ++++----- .../domain/remote/DailyWeatherDataDto.kt | 18 ++++--- .../domain/remote/HourlyWeatherDataDto.kt | 12 ++--- .../qweather/domain/remote/WeatherApi.kt | 2 +- .../domain/weather/DailyWeatherData.kt | 2 + .../components/weather/WeatherDay.kt | 53 ++++++++++++++++--- .../screenmodel/DailyWeatherScreenModel.kt | 7 ++- .../screenmodel/HourlyWeatherScreenModel.kt | 8 ++- .../qweather/presentation/tabs/TodayTab.kt | 13 +++-- .../qweather/presentation/tabs/WeekTab.kt | 15 +++--- app/src/main/res/values/strings.xml | 4 ++ 14 files changed, 150 insertions(+), 95 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd5cfe1..2bb8b00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { applicationId = "com.henryhiles.qweather" - minSdk = 21 + minSdk = 30 targetSdk = 33 versionCode = 1 versionName = "1.0" @@ -57,19 +57,28 @@ android { } dependencies { - val composeVersion = "1.4.0" - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.compose.ui:ui:$composeVersion") - implementation("androidx.compose.material3:material3:1.1.0-beta01") - implementation("androidx.compose.material:material-icons-extended:$composeVersion") - implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") + implementation("androidx.core:core-ktx:1.10.0") + implementation("androidx.compose.material3:material3:1.1.0-beta02") implementation("androidx.activity:activity-compose:1.7.0") - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + implementation("androidx.core:core-ktx:1.10.0") + + implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + + // Lifecycle + val lifecycleVersion = "2.6.1" + + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") + + // Compose + val composeVersion = "1.4.0" + + implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") + implementation("androidx.compose.ui:ui:$composeVersion") + implementation("androidx.compose.material:material-icons-extended:$composeVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") // Voyager val voyagerVersion = "1.0.0-rc04" @@ -88,9 +97,9 @@ dependencies { implementation("io.insert-koin:koin-androidx-compose:$koinVersion") // Retrofit - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-moshi:2.9.0") - implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") + val retrofitVersion = "2.9.0" + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.retrofit2:converter-moshi:$retrofitVersion") // Accompanist val accompanistVersion = "0.30.0" diff --git a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt index addceb0..3f95b22 100644 --- a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt +++ b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt @@ -3,7 +3,6 @@ package com.henryhiles.qweather.di import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities -import android.os.Build import com.henryhiles.qweather.domain.remote.WeatherApi import okhttp3.Cache import okhttp3.Interceptor @@ -17,19 +16,15 @@ import retrofit2.create private fun isNetworkAvailable(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val networkCapabilities = connectivityManager.activeNetwork ?: return false - val activeNetwork = - connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false - return when { - activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true - activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true - activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true - else -> false - } - } else { - val activeNetworkInfo = connectivityManager.activeNetworkInfo ?: return false - return activeNetworkInfo.isConnected + + val networkCapabilities = connectivityManager.activeNetwork ?: return false + val activeNetwork = + connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + return when { + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false } } diff --git a/app/src/main/java/com/henryhiles/qweather/domain/location/LocationTracker.kt b/app/src/main/java/com/henryhiles/qweather/domain/location/LocationTracker.kt index 8aee9fb..6c21a6c 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/location/LocationTracker.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/location/LocationTracker.kt @@ -23,28 +23,7 @@ class LocationTracker constructor( LocationManager.GPS_PROVIDER ) if (!hasAccessFineLocationPermission || !isGpsEnabled) return null - + return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) -// return suspendCancellableCoroutine { cont -> -// locationManager.getLastKnownLocation.apply { -// if (isComplete) { -// if (isSuccessful) { -// cont.resume(result) -// } else { -// cont.resume(null) -// } -// return@suspendCancellableCoroutine -// } -// addOnSuccessListener { -// cont.resume(it) -// } -// addOnFailureListener { -// cont.resume(null) -// } -// addOnCanceledListener { -// cont.cancel() -// } -// } -// } } } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/mappers/WeatherMappers.kt b/app/src/main/java/com/henryhiles/qweather/domain/mappers/WeatherMappers.kt index cec0ef9..76f5002 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/mappers/WeatherMappers.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/mappers/WeatherMappers.kt @@ -15,30 +15,32 @@ import kotlin.math.roundToInt private data class IndexedHourlyWeatherData(val index: Int, val data: HourlyWeatherData) fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): Map> { - return times.mapIndexed { index, time -> + return time.mapIndexed { index, time -> IndexedHourlyWeatherData( index = index, data = HourlyWeatherData( time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME), - temperature = temperatures[index].roundToInt(), - apparentTemperature = apparentTemperatures[index].roundToInt(), - windSpeed = windSpeeds[index].roundToInt(), - precipitationProbability = precipitationProbabilities.getOrNull(index), - weatherType = WeatherType.fromWMO(weatherCodes[index]) + temperature = temperature[index].roundToInt(), + apparentTemperature = apparentTemperature[index].roundToInt(), + windSpeed = windSpeed[index].roundToInt(), + precipitationProbability = precipitationProbability.getOrNull(index), + weatherType = WeatherType.fromWMO(weatherCode[index]) ) ) }.groupBy { it.index / 24 }.mapValues { entry -> entry.value.map { it.data } } } fun DailyWeatherDataDto.toDailyWeatherDataMap(): List { - return dates.mapIndexed { index, date -> + return date.mapIndexed { index, date -> DailyWeatherData( date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE), - weatherType = WeatherType.fromWMO(weatherCodes[index]), - apparentTemperatureMax = apparentTemperaturesMax[index].roundToInt(), - apparentTemperatureMin = apparentTemperaturesMin[index].roundToInt(), - temperatureMax = temperaturesMax[index].roundToInt(), - temperatureMin = temperaturesMin[index].roundToInt() + weatherType = WeatherType.fromWMO(weatherCode[index]), + apparentTemperatureMax = apparentTemperatureMax[index].roundToInt(), + apparentTemperatureMin = apparentTemperatureMin[index].roundToInt(), + temperatureMax = temperatureMax[index].roundToInt(), + temperatureMin = temperatureMin[index].roundToInt(), + precipitationProbabilityMax = precipitationProbabilityMax.getOrNull(index), + windSpeedMax = windSpeedMax[index].roundToInt() ) } } diff --git a/app/src/main/java/com/henryhiles/qweather/domain/remote/DailyWeatherDataDto.kt b/app/src/main/java/com/henryhiles/qweather/domain/remote/DailyWeatherDataDto.kt index 0f6cf9b..336e5b6 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/remote/DailyWeatherDataDto.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/remote/DailyWeatherDataDto.kt @@ -4,15 +4,21 @@ import com.squareup.moshi.Json data class DailyWeatherDataDto( @field:Json(name = "time") - val dates: List, + val date: List, @field:Json(name = "weathercode") - val weatherCodes: List, + val weatherCode: List, + @field:Json(name = "precipitation_probability_max") + val precipitationProbabilityMax: List, + @field:Json(name = "precipitation_sum") + val precipitationSum: List, + @field:Json(name = "windspeed_10m_max") + val windSpeedMax: List, @field:Json(name = "temperature_2m_max") - val temperaturesMax: List, + val temperatureMax: List, @field:Json(name = "temperature_2m_min") - val temperaturesMin: List, + val temperatureMin: List, @field:Json(name = "apparent_temperature_max") - val apparentTemperaturesMax: List, + val apparentTemperatureMax: List, @field:Json(name = "apparent_temperature_min") - val apparentTemperaturesMin: List + val apparentTemperatureMin: List ) \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/remote/HourlyWeatherDataDto.kt b/app/src/main/java/com/henryhiles/qweather/domain/remote/HourlyWeatherDataDto.kt index 9f56652..2b2a8fd 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/remote/HourlyWeatherDataDto.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/remote/HourlyWeatherDataDto.kt @@ -4,15 +4,15 @@ import com.squareup.moshi.Json data class HourlyWeatherDataDto( @field:Json(name = "time") - val times: List, + val time: List, @field:Json(name = "temperature_2m") - val temperatures: List, + val temperature: List, @field:Json(name = "apparent_temperature") - val apparentTemperatures: List, + val apparentTemperature: List, @field:Json(name = "weathercode") - val weatherCodes: List, + val weatherCode: List, @field:Json(name = "precipitation_probability") - val precipitationProbabilities: List, + val precipitationProbability: List, @field:Json(name = "windspeed_10m") - val windSpeeds: List, + val windSpeed: List, ) \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt b/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt index 88c32e5..7958a0c 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/remote/WeatherApi.kt @@ -5,7 +5,7 @@ import retrofit2.http.Headers import retrofit2.http.Query const val DAILY = - "daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min" + "daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max" const val HOURLY = "hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m" const val TIMEZONE = "timezone=auto" diff --git a/app/src/main/java/com/henryhiles/qweather/domain/weather/DailyWeatherData.kt b/app/src/main/java/com/henryhiles/qweather/domain/weather/DailyWeatherData.kt index 492c217..0940e5a 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/weather/DailyWeatherData.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/weather/DailyWeatherData.kt @@ -9,4 +9,6 @@ data class DailyWeatherData( val temperatureMin: Int, val apparentTemperatureMax: Int, val apparentTemperatureMin: Int, + val precipitationProbabilityMax: Int?, + val windSpeedMax: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDay.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDay.kt index 9609333..8fcafa6 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDay.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDay.kt @@ -1,7 +1,11 @@ package com.henryhiles.qweather.presentation.components.weather import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Water +import androidx.compose.material.icons.outlined.WaterDrop import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -11,13 +15,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp +import com.henryhiles.qweather.R import com.henryhiles.qweather.domain.weather.DailyWeatherData import java.time.format.DateTimeFormatter @Composable -fun WeatherDay(dailyWeatherData: DailyWeatherData) { +fun WeatherDay(dailyWeatherData: DailyWeatherData, expanded: Boolean, onExpand: () -> Unit) { val formattedDate by remember { derivedStateOf { dailyWeatherData.date.format( @@ -27,10 +34,12 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) { } Card( - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 8.dp - ) + modifier = Modifier + .padding( + horizontal = 16.dp, + vertical = 8.dp + ) + .clickable(onClick = onExpand) ) { Row( modifier = Modifier @@ -40,9 +49,10 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) { ) { Image( painter = painterResource(id = dailyWeatherData.weatherType.iconRes), - contentDescription = "Image of ${dailyWeatherData.weatherType}", + contentDescription = "Image of ${dailyWeatherData.weatherType.weatherDesc}", modifier = Modifier.width(48.dp) ) + Spacer(modifier = Modifier.width(16.dp)) Column { Text( @@ -57,5 +67,36 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) { style = MaterialTheme.typography.titleLarge, ) } + if (expanded) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp, 0.dp, 16.dp, 16.dp), + horizontalArrangement = Arrangement.Center + ) { + WeatherDataDisplay( + value = dailyWeatherData.precipitationProbabilityMax, + unit = "%", + icon = Icons.Outlined.WaterDrop, + description = "Chance of rain" + ) + + Spacer(modifier = Modifier.width(16.dp)) + WeatherDataDisplay( + value = dailyWeatherData.windSpeedMax, + unit = "mm", + icon = Icons.Outlined.Water, + description = "Precipitation Amount" + ) + + Spacer(modifier = Modifier.width(16.dp)) + WeatherDataDisplay( + value = dailyWeatherData.windSpeedMax, + unit = "km/h", + icon = ImageVector.vectorResource(id = R.drawable.ic_wind), + description = "Wind Speed" + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt index 62b04ad..48c312a 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/DailyWeatherScreenModel.kt @@ -15,7 +15,8 @@ import kotlinx.coroutines.launch data class DailyWeatherState( val dailyWeatherData: List? = null, val isLoading: Boolean = false, - val error: String? = null + val error: String? = null, + val expanded: Int? = null ) class DailyWeatherScreenModel constructor( @@ -60,4 +61,8 @@ class DailyWeatherScreenModel constructor( } } } + + fun setExpanded(index: Int?) { + state = state.copy(expanded = index) + } } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt index 8e72888..919b0df 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/HourlyWeatherScreenModel.kt @@ -1,10 +1,12 @@ package com.henryhiles.qweather.presentation.screenmodel +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.coroutineScope +import com.henryhiles.qweather.R import com.henryhiles.qweather.domain.location.LocationTracker import com.henryhiles.qweather.domain.repository.WeatherRepository import com.henryhiles.qweather.domain.util.Resource @@ -14,12 +16,14 @@ import kotlinx.coroutines.launch data class HourlyWeatherState( val hourlyWeatherInfo: HourlyWeatherInfo? = null, val isLoading: Boolean = false, - val error: String? = null + val error: String? = null, + val expanded: Int? = null ) class HourlyWeatherScreenModel constructor( private val repository: WeatherRepository, private val locationTracker: LocationTracker, + private val context: Context ) : ScreenModel { var state by mutableStateOf(HourlyWeatherState()) private set @@ -54,7 +58,7 @@ class HourlyWeatherScreenModel constructor( } ?: kotlin.run { state = state.copy( isLoading = false, - error = "Couldn't retrieve location. Make sure to grant permission and enable GPS." + error = context.getString(R.string.error_location) ) } } diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt index 6105498..a2fa590 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt @@ -45,7 +45,6 @@ object TodayTab : NavigationTab { @Composable override fun Content() { val weatherViewModel = getScreenModel() - val permissionsState = rememberPermissionState( Manifest.permission.ACCESS_FINE_LOCATION, ) { @@ -68,14 +67,20 @@ object TodayTab : NavigationTab { weatherViewModel.state.error != null -> { AlertDialog( onDismissRequest = {}, - confirmButton = {}, - title = { Text(text = "An error occurred") }, text = { + confirmButton = { + TextButton(onClick = { weatherViewModel.loadWeatherInfo() }) { + Text(text = stringResource(id = R.string.action_try_again)) + } + }, + title = { Text(text = stringResource(id = R.string.error)) }, + text = { SelectionContainer { Text( text = weatherViewModel.state.error!!, ) } - }) + }, + ) } else -> { Column( diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt index 1ab9100..ca72062 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt @@ -3,15 +3,13 @@ package com.henryhiles.qweather.presentation.tabs import android.Manifest import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter @@ -84,8 +82,13 @@ object WeekTab : NavigationTab { .fillMaxSize() ) { weatherViewModel.state.dailyWeatherData?.let { data -> - items(data) { - WeatherDay(dailyWeatherData = it) + itemsIndexed(data) { index, dailyData -> + val expanded = weatherViewModel.state.expanded == index + WeatherDay( + dailyWeatherData = dailyData, + expanded = expanded, + onExpand = { weatherViewModel.setExpanded(if (expanded) null else index) } + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c560d21..21e8835 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Confirm About Reload + Try Again Theme Dynamic Theme @@ -21,4 +22,7 @@ Dark Unknown + + An error occurred + Couldn\'t retrieve location. Make sure to grant permission and enable GPS. \ No newline at end of file