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 { defaultConfig {
applicationId = "com.henryhiles.qweather" applicationId = "com.henryhiles.qweather"
minSdk = 21 minSdk = 30
targetSdk = 33 targetSdk = 33
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -57,19 +57,28 @@ android {
} }
dependencies { dependencies {
val composeVersion = "1.4.0" implementation("androidx.core:core-ktx:1.10.0")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.compose.material3:material3:1.1.0-beta02")
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.activity:activity-compose:1.7.0") implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.10.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
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") 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 // Voyager
val voyagerVersion = "1.0.0-rc04" val voyagerVersion = "1.0.0-rc04"
@ -88,9 +97,9 @@ dependencies {
implementation("io.insert-koin:koin-androidx-compose:$koinVersion") implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
// Retrofit // Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0") val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:converter-moshi:2.9.0") implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") implementation("com.squareup.retrofit2:converter-moshi:$retrofitVersion")
// Accompanist // Accompanist
val accompanistVersion = "0.30.0" val accompanistVersion = "0.30.0"

View file

@ -3,7 +3,6 @@ package com.henryhiles.qweather.di
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
import com.henryhiles.qweather.domain.remote.WeatherApi import com.henryhiles.qweather.domain.remote.WeatherApi
import okhttp3.Cache import okhttp3.Cache
import okhttp3.Interceptor import okhttp3.Interceptor
@ -17,7 +16,7 @@ import retrofit2.create
private fun isNetworkAvailable(context: Context): Boolean { private fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager = val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.activeNetwork ?: return false val networkCapabilities = connectivityManager.activeNetwork ?: return false
val activeNetwork = val activeNetwork =
connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
@ -27,10 +26,6 @@ private fun isNetworkAvailable(context: Context): Boolean {
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false else -> false
} }
} else {
val activeNetworkInfo = connectivityManager.activeNetworkInfo ?: return false
return activeNetworkInfo.isConnected
}
} }
val appModule = module { val appModule = module {

View file

@ -25,26 +25,5 @@ class LocationTracker constructor(
if (!hasAccessFineLocationPermission || !isGpsEnabled) return null if (!hasAccessFineLocationPermission || !isGpsEnabled) return null
return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) 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) private data class IndexedHourlyWeatherData(val index: Int, val data: HourlyWeatherData)
fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): Map<Int, List<HourlyWeatherData>> { fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): Map<Int, List<HourlyWeatherData>> {
return times.mapIndexed { index, time -> return time.mapIndexed { index, time ->
IndexedHourlyWeatherData( IndexedHourlyWeatherData(
index = index, index = index,
data = HourlyWeatherData( data = HourlyWeatherData(
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME), time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
temperature = temperatures[index].roundToInt(), temperature = temperature[index].roundToInt(),
apparentTemperature = apparentTemperatures[index].roundToInt(), apparentTemperature = apparentTemperature[index].roundToInt(),
windSpeed = windSpeeds[index].roundToInt(), windSpeed = windSpeed[index].roundToInt(),
precipitationProbability = precipitationProbabilities.getOrNull(index), precipitationProbability = precipitationProbability.getOrNull(index),
weatherType = WeatherType.fromWMO(weatherCodes[index]) weatherType = WeatherType.fromWMO(weatherCode[index])
) )
) )
}.groupBy { it.index / 24 }.mapValues { entry -> entry.value.map { it.data } } }.groupBy { it.index / 24 }.mapValues { entry -> entry.value.map { it.data } }
} }
fun DailyWeatherDataDto.toDailyWeatherDataMap(): List<DailyWeatherData> { fun DailyWeatherDataDto.toDailyWeatherDataMap(): List<DailyWeatherData> {
return dates.mapIndexed { index, date -> return date.mapIndexed { index, date ->
DailyWeatherData( DailyWeatherData(
date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE), date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
weatherType = WeatherType.fromWMO(weatherCodes[index]), weatherType = WeatherType.fromWMO(weatherCode[index]),
apparentTemperatureMax = apparentTemperaturesMax[index].roundToInt(), apparentTemperatureMax = apparentTemperatureMax[index].roundToInt(),
apparentTemperatureMin = apparentTemperaturesMin[index].roundToInt(), apparentTemperatureMin = apparentTemperatureMin[index].roundToInt(),
temperatureMax = temperaturesMax[index].roundToInt(), temperatureMax = temperatureMax[index].roundToInt(),
temperatureMin = temperaturesMin[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( data class DailyWeatherDataDto(
@field:Json(name = "time") @field:Json(name = "time")
val dates: List<String>, val date: List<String>,
@field:Json(name = "weathercode") @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") @field:Json(name = "temperature_2m_max")
val temperaturesMax: List<Double>, val temperatureMax: List<Double>,
@field:Json(name = "temperature_2m_min") @field:Json(name = "temperature_2m_min")
val temperaturesMin: List<Double>, val temperatureMin: List<Double>,
@field:Json(name = "apparent_temperature_max") @field:Json(name = "apparent_temperature_max")
val apparentTemperaturesMax: List<Double>, val apparentTemperatureMax: List<Double>,
@field:Json(name = "apparent_temperature_min") @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( data class HourlyWeatherDataDto(
@field:Json(name = "time") @field:Json(name = "time")
val times: List<String>, val time: List<String>,
@field:Json(name = "temperature_2m") @field:Json(name = "temperature_2m")
val temperatures: List<Double>, val temperature: List<Double>,
@field:Json(name = "apparent_temperature") @field:Json(name = "apparent_temperature")
val apparentTemperatures: List<Double>, val apparentTemperature: List<Double>,
@field:Json(name = "weathercode") @field:Json(name = "weathercode")
val weatherCodes: List<Int>, val weatherCode: List<Int>,
@field:Json(name = "precipitation_probability") @field:Json(name = "precipitation_probability")
val precipitationProbabilities: List<Int>, val precipitationProbability: List<Int>,
@field:Json(name = "windspeed_10m") @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 import retrofit2.http.Query
const val DAILY = 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 = const val HOURLY =
"hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m" "hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m"
const val TIMEZONE = "timezone=auto" const val TIMEZONE = "timezone=auto"

View file

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

View file

@ -1,7 +1,11 @@
package com.henryhiles.qweather.presentation.components.weather package com.henryhiles.qweather.presentation.components.weather
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.Card
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -11,13 +15,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Composable @Composable
fun WeatherDay(dailyWeatherData: DailyWeatherData) { fun WeatherDay(dailyWeatherData: DailyWeatherData, expanded: Boolean, onExpand: () -> Unit) {
val formattedDate by remember { val formattedDate by remember {
derivedStateOf { derivedStateOf {
dailyWeatherData.date.format( dailyWeatherData.date.format(
@ -27,10 +34,12 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
} }
Card( Card(
modifier = Modifier.padding( modifier = Modifier
.padding(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 8.dp vertical = 8.dp
) )
.clickable(onClick = onExpand)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -40,9 +49,10 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
) { ) {
Image( Image(
painter = painterResource(id = dailyWeatherData.weatherType.iconRes), painter = painterResource(id = dailyWeatherData.weatherType.iconRes),
contentDescription = "Image of ${dailyWeatherData.weatherType}", contentDescription = "Image of ${dailyWeatherData.weatherType.weatherDesc}",
modifier = Modifier.width(48.dp) modifier = Modifier.width(48.dp)
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column { Column {
Text( Text(
@ -57,5 +67,36 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
style = MaterialTheme.typography.titleLarge, 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( data class DailyWeatherState(
val dailyWeatherData: List<DailyWeatherData>? = null, val dailyWeatherData: List<DailyWeatherData>? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null,
val expanded: Int? = null
) )
class DailyWeatherScreenModel constructor( 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 package com.henryhiles.qweather.presentation.screenmodel
import android.content.Context
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.location.LocationTracker import com.henryhiles.qweather.domain.location.LocationTracker
import com.henryhiles.qweather.domain.repository.WeatherRepository import com.henryhiles.qweather.domain.repository.WeatherRepository
import com.henryhiles.qweather.domain.util.Resource import com.henryhiles.qweather.domain.util.Resource
@ -14,12 +16,14 @@ import kotlinx.coroutines.launch
data class HourlyWeatherState( data class HourlyWeatherState(
val hourlyWeatherInfo: HourlyWeatherInfo? = null, val hourlyWeatherInfo: HourlyWeatherInfo? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null,
val expanded: Int? = null
) )
class HourlyWeatherScreenModel constructor( class HourlyWeatherScreenModel constructor(
private val repository: WeatherRepository, private val repository: WeatherRepository,
private val locationTracker: LocationTracker, private val locationTracker: LocationTracker,
private val context: Context
) : ScreenModel { ) : ScreenModel {
var state by mutableStateOf(HourlyWeatherState()) var state by mutableStateOf(HourlyWeatherState())
private set private set
@ -54,7 +58,7 @@ class HourlyWeatherScreenModel constructor(
} ?: kotlin.run { } ?: kotlin.run {
state = state.copy( state = state.copy(
isLoading = false, 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 @Composable
override fun Content() { override fun Content() {
val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>() val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val permissionsState = rememberPermissionState( val permissionsState = rememberPermissionState(
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION,
) { ) {
@ -68,14 +67,20 @@ object TodayTab : NavigationTab {
weatherViewModel.state.error != null -> { weatherViewModel.state.error != null -> {
AlertDialog( AlertDialog(
onDismissRequest = {}, onDismissRequest = {},
confirmButton = {}, confirmButton = {
title = { Text(text = "An error occurred") }, text = { TextButton(onClick = { weatherViewModel.loadWeatherInfo() }) {
Text(text = stringResource(id = R.string.action_try_again))
}
},
title = { Text(text = stringResource(id = R.string.error)) },
text = {
SelectionContainer { SelectionContainer {
Text( Text(
text = weatherViewModel.state.error!!, text = weatherViewModel.state.error!!,
) )
} }
}) },
)
} }
else -> { else -> {
Column( Column(

View file

@ -3,15 +3,13 @@ package com.henryhiles.qweather.presentation.tabs
import android.Manifest import android.Manifest
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
@ -84,8 +82,13 @@ object WeekTab : NavigationTab {
.fillMaxSize() .fillMaxSize()
) { ) {
weatherViewModel.state.dailyWeatherData?.let { data -> weatherViewModel.state.dailyWeatherData?.let { data ->
items(data) { itemsIndexed(data) { index, dailyData ->
WeatherDay(dailyWeatherData = it) 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_confirm">Confirm</string>
<string name="action_open_about">About</string> <string name="action_open_about">About</string>
<string name="action_reload">Reload</string> <string name="action_reload">Reload</string>
<string name="action_try_again">Try Again</string>
<string name="appearance_theme">Theme</string> <string name="appearance_theme">Theme</string>
<string name="appearance_monet">Dynamic Theme</string> <string name="appearance_monet">Dynamic Theme</string>
@ -21,4 +22,7 @@
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="unknown">Unknown</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> </resources>