many changes including expandable days, also bump dependencies
This commit is contained in:
parent
0b3254877b
commit
dc1d3328cb
14 changed files with 150 additions and 95 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
|
@ -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>,
|
||||
)
|
|
@ -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"
|
||||
|
|
|
@ -9,4 +9,6 @@ data class DailyWeatherData(
|
|||
val temperatureMin: Int,
|
||||
val apparentTemperatureMax: Int,
|
||||
val apparentTemperatureMin: Int,
|
||||
val precipitationProbabilityMax: Int?,
|
||||
val windSpeedMax: Int
|
||||
)
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
Reference in a new issue