many changes including expandable days, also bump dependencies

This commit is contained in:
Henry Hiles 2023-04-11 10:23:00 -04:00
parent 0b3254877b
commit dc1d3328cb
14 changed files with 150 additions and 95 deletions

View file

@ -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"

View file

@ -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
}
}

View file

@ -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()
// }
// }
// }
}
}

View file

@ -15,30 +15,32 @@ import kotlin.math.roundToInt
private data class IndexedHourlyWeatherData(val index: Int, val data: HourlyWeatherData)
fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): Map<Int, List<HourlyWeatherData>> {
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<DailyWeatherData> {
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()
)
}
}

View file

@ -4,15 +4,21 @@ import com.squareup.moshi.Json
data class DailyWeatherDataDto(
@field:Json(name = "time")
val dates: List<String>,
val date: List<String>,
@field:Json(name = "weathercode")
val weatherCodes: List<Int>,
val weatherCode: List<Int>,
@field:Json(name = "precipitation_probability_max")
val precipitationProbabilityMax: List<Int>,
@field:Json(name = "precipitation_sum")
val precipitationSum: List<Double>,
@field:Json(name = "windspeed_10m_max")
val windSpeedMax: List<Double>,
@field:Json(name = "temperature_2m_max")
val temperaturesMax: List<Double>,
val temperatureMax: List<Double>,
@field:Json(name = "temperature_2m_min")
val temperaturesMin: List<Double>,
val temperatureMin: List<Double>,
@field:Json(name = "apparent_temperature_max")
val apparentTemperaturesMax: List<Double>,
val apparentTemperatureMax: List<Double>,
@field:Json(name = "apparent_temperature_min")
val apparentTemperaturesMin: List<Double>
val apparentTemperatureMin: List<Double>
)

View file

@ -4,15 +4,15 @@ import com.squareup.moshi.Json
data class HourlyWeatherDataDto(
@field:Json(name = "time")
val times: List<String>,
val time: List<String>,
@field:Json(name = "temperature_2m")
val temperatures: List<Double>,
val temperature: List<Double>,
@field:Json(name = "apparent_temperature")
val apparentTemperatures: List<Double>,
val apparentTemperature: List<Double>,
@field:Json(name = "weathercode")
val weatherCodes: List<Int>,
val weatherCode: List<Int>,
@field:Json(name = "precipitation_probability")
val precipitationProbabilities: List<Int>,
val precipitationProbability: List<Int>,
@field:Json(name = "windspeed_10m")
val windSpeeds: List<Double>,
val windSpeed: List<Double>,
)

View file

@ -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"

View file

@ -9,4 +9,6 @@ data class DailyWeatherData(
val temperatureMin: Int,
val apparentTemperatureMax: Int,
val apparentTemperatureMin: Int,
val precipitationProbabilityMax: Int?,
val windSpeedMax: Int
)

View file

@ -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"
)
}
}
}
}

View file

@ -15,7 +15,8 @@ import kotlinx.coroutines.launch
data class DailyWeatherState(
val dailyWeatherData: List<DailyWeatherData>? = 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)
}
}

View file

@ -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)
)
}
}

View file

@ -45,7 +45,6 @@ object TodayTab : NavigationTab {
@Composable
override fun Content() {
val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
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(

View file

@ -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) }
)
}
}
}

View file

@ -8,6 +8,7 @@
<string name="action_confirm">Confirm</string>
<string name="action_open_about">About</string>
<string name="action_reload">Reload</string>
<string name="action_try_again">Try Again</string>
<string name="appearance_theme">Theme</string>
<string name="appearance_monet">Dynamic Theme</string>
@ -21,4 +22,7 @@
<string name="theme_dark">Dark</string>
<string name="unknown">Unknown</string>
<string name="error">An error occurred</string>
<string name="error_location">Couldn\'t retrieve location. Make sure to grant permission and enable GPS.</string>
</resources>