not working, nullpointerexception

This commit is contained in:
Henry Hiles 2023-04-06 14:32:16 -04:00
parent 812e627abb
commit 78285ab5c2
24 changed files with 353 additions and 118 deletions

View file

@ -1,9 +1,10 @@
package com.henryhiles.qweather.di package com.henryhiles.qweather.di
import com.henryhiles.qweather.domain.remote.WeatherApi import com.henryhiles.qweather.domain.remote.WeatherApi
import com.henryhiles.qweather.presentation.screenmodel.AppearanceSettingsScreenModel import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.PreferenceManager import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -35,8 +36,9 @@ val appModule = module {
} }
singleOf(::provideWeatherApi) singleOf(::provideWeatherApi)
singleOf(::PreferenceManager) singleOf(::AppearancePreferenceManager)
factoryOf(::AppearanceSettingsScreenModel)
factoryOf(::WeatherScreenModel) factoryOf(::AppearancePreferencesScreenModel)
factoryOf(::HourlyWeatherScreenModel)
factoryOf(::DailyWeatherScreenModel)
} }

View file

@ -1,35 +1,56 @@
package com.henryhiles.qweather.domain.mappers package com.henryhiles.qweather.domain.mappers
import com.henryhiles.qweather.domain.remote.WeatherDataDto import com.henryhiles.qweather.domain.remote.DailyWeatherDataDto
import com.henryhiles.qweather.domain.remote.HourlyWeatherDataDto
import com.henryhiles.qweather.domain.remote.WeatherDto import com.henryhiles.qweather.domain.remote.WeatherDto
import com.henryhiles.qweather.domain.weather.WeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.WeatherInfo import com.henryhiles.qweather.domain.weather.HourlyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
import com.henryhiles.qweather.domain.weather.WeatherType import com.henryhiles.qweather.domain.weather.WeatherType
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
private data class IndexedWeatherData(val index: Int, val data: WeatherData) private data class IndexedHourlyWeatherData(val index: Int, val data: HourlyWeatherData)
fun WeatherDataDto.toWeatherDataMap(): Map<Int, List<WeatherData>> { fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): Map<Int, List<HourlyWeatherData>> {
return time.mapIndexed { index, time -> return times.mapIndexed { index, time ->
IndexedWeatherData( IndexedHourlyWeatherData(
index = index, data = WeatherData( index = index,
data = HourlyWeatherData(
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME), time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
temperatureCelsius = temperatures[index].toInt(), temperature = temperatures[index].roundToInt(),
pressure = pressures[index], apparentTemperature = apparentTemperatures[index].roundToInt(),
windSpeed = windSpeeds[index], windSpeed = windSpeeds[index].roundToInt(),
humidity = humidities[index], precipitationProbability = precipitationProbabilities[index],
weatherType = WeatherType.fromWMO(weatherCodes[index]) weatherType = WeatherType.fromWMO(weatherCodes[index])
) )
) )
}.groupBy { it.index / 24 }.mapValues { entry -> entry.value.map { it.data } } }.groupBy { it.index / 24 }.mapValues { entry -> entry.value.map { it.data } }
} }
fun WeatherDto.toWeatherInfo(): WeatherInfo { fun DailyWeatherDataDto.toDailyWeatherDataMap(): List<DailyWeatherData> {
val weatherDataMap = weatherData.toWeatherDataMap() return dates.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()
)
}
}
fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo {
val weatherDataMap = hourlyWeatherData.toHourlyWeatherDataMap()
val now = LocalDateTime.now() val now = LocalDateTime.now()
val currentWeatherData = weatherDataMap[0]?.find { val currentWeatherData = weatherDataMap[0]?.find {
it.time.hour == now.hour it.time.hour == now.hour
} }
return WeatherInfo(weatherDataPerDay = weatherDataMap, currentWeatherData = currentWeatherData) return HourlyWeatherInfo(
weatherDataPerDay = weatherDataMap,
currentWeatherData = currentWeatherData
)
} }

View file

@ -0,0 +1,18 @@
package com.henryhiles.qweather.domain.remote
import com.squareup.moshi.Json
data class DailyWeatherDataDto(
@field:Json(name = "time")
val dates: List<String>,
@field:Json(name = "weathercode")
val weatherCodes: List<Int>,
@field:Json(name = "temperature_2m_max")
val temperaturesMax: List<Double>,
@field:Json(name = "temperature_2m_min")
val temperaturesMin: List<Double>,
@field:Json(name = "apparent_temperature_max")
val apparentTemperaturesMax: List<Double>,
@field:Json(name = "apparent_temperature_min")
val apparentTemperaturesMin: List<Double>
)

View file

@ -2,16 +2,17 @@ package com.henryhiles.qweather.domain.remote
import com.squareup.moshi.Json import com.squareup.moshi.Json
data class WeatherDataDto( data class HourlyWeatherDataDto(
val time: List<String>, @field:Json(name = "time")
val times: List<String>,
@field:Json(name = "temperature_2m") @field:Json(name = "temperature_2m")
val temperatures: List<Double>, val temperatures: List<Double>,
@field:Json(name = "apparent_temperature")
val apparentTemperatures: List<Double>,
@field:Json(name = "weathercode") @field:Json(name = "weathercode")
val weatherCodes: List<Int>, val weatherCodes: List<Int>,
@field:Json(name = "pressure_msl") @field:Json(name = "precipitation_probability")
val pressures: List<Double>, val precipitationProbabilities: List<Int>,
@field:Json(name = "windspeed_10m") @field:Json(name = "windspeed_10m")
val windSpeeds: List<Double>, val windSpeeds: List<Double>,
@field:Json(name = "relativehumidity_2m")
val humidities: List<Double>
) )

View file

@ -3,10 +3,17 @@ package com.henryhiles.qweather.domain.remote
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
const val DAILY =
"daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min"
const val HOURLY =
"hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m"
const val TIMEZONE = "timezone=auto"
const val FORECAST_DAYS = "forecast_days=14"
interface WeatherApi { interface WeatherApi {
@GET("v1/forecast?latitude=43.72&longitude=-79.35&hourly=temperature_2m&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=auto") @GET("v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS")
suspend fun getWeatherData( suspend fun getWeatherData(
@Query("latitude") lat: Double, @Query("latitude") lat: Double,
@Query("longitude") long: Double @Query("longitude") long: Double,
): WeatherDto ): WeatherDto
} }

View file

@ -4,5 +4,8 @@ import com.squareup.moshi.Json
data class WeatherDto( data class WeatherDto(
@field:Json(name = "hourly") @field:Json(name = "hourly")
val weatherData: WeatherDataDto val hourlyWeatherData: HourlyWeatherDataDto,
@field:Json(name = "daily")
val dailyWeatherData: DailyWeatherDataDto
) )

View file

@ -1,14 +1,35 @@
package com.henryhiles.qweather.domain.repository package com.henryhiles.qweather.domain.repository
import com.henryhiles.qweather.domain.mappers.toWeatherInfo import com.henryhiles.qweather.domain.mappers.toDailyWeatherDataMap
import com.henryhiles.qweather.domain.mappers.toHourlyWeatherInfo
import com.henryhiles.qweather.domain.remote.WeatherApi import com.henryhiles.qweather.domain.remote.WeatherApi
import com.henryhiles.qweather.domain.util.Resource import com.henryhiles.qweather.domain.util.Resource
import com.henryhiles.qweather.domain.weather.WeatherInfo import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
class WeatherRepository constructor(private val api: WeatherApi) { class WeatherRepository constructor(private val api: WeatherApi) {
suspend fun getWeatherData(lat: Double, long: Double): Resource<WeatherInfo> { suspend fun getHourlyWeatherData(lat: Double, long: Double): Resource<HourlyWeatherInfo> {
return try { return try {
Resource.Success(data = api.getWeatherData(lat = lat, long = long).toWeatherInfo()) Resource.Success(
data = api.getWeatherData(lat = lat, long = long).toHourlyWeatherInfo()
)
} catch (e: Exception) {
e.printStackTrace()
Resource.Error(e.message ?: "An unknown error occurred.")
}
}
suspend fun getDailyWeatherData(
lat: Double,
long: Double
): Resource<List<DailyWeatherData>> {
return try {
Resource.Success(
data = api.getWeatherData(
lat = lat,
long = long
).dailyWeatherData.toDailyWeatherDataMap()
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Resource.Error(e.message ?: "An unknown error occurred.") Resource.Error(e.message ?: "An unknown error occurred.")

View file

@ -0,0 +1,12 @@
package com.henryhiles.qweather.domain.weather
import java.time.LocalDate
data class DailyWeatherData(
val date: LocalDate,
val weatherType: WeatherType,
val temperatureMax: Int,
val temperatureMin: Int,
val apparentTemperatureMax: Int,
val apparentTemperatureMin: Int,
)

View file

@ -0,0 +1,12 @@
package com.henryhiles.qweather.domain.weather
import java.time.LocalDateTime
data class HourlyWeatherData(
val time: LocalDateTime,
val temperature: Int,
val apparentTemperature: Int,
val weatherType: WeatherType,
val precipitationProbability: Int?,
val windSpeed: Int,
)

View file

@ -0,0 +1,6 @@
package com.henryhiles.qweather.domain.weather
data class HourlyWeatherInfo(
val weatherDataPerDay: Map<Int, List<HourlyWeatherData>>,
val currentWeatherData: HourlyWeatherData?
)

View file

@ -1,12 +0,0 @@
package com.henryhiles.qweather.domain.weather
import java.time.LocalDateTime
data class WeatherData(
val time: LocalDateTime,
val temperatureCelsius: Int,
val pressure: Double,
val windSpeed: Double,
val humidity: Double,
val weatherType: WeatherType
)

View file

@ -1,6 +0,0 @@
package com.henryhiles.qweather.domain.weather
data class WeatherInfo(
val weatherDataPerDay: Map<Int, List<WeatherData>>,
val currentWeatherData: WeatherData?
)

View file

@ -9,13 +9,13 @@ import androidx.compose.material3.Surface
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.SlideTransition import cafe.adriel.voyager.transitions.SlideTransition
import com.henryhiles.qweather.presentation.screen.MainScreen import com.henryhiles.qweather.presentation.screen.MainScreen
import com.henryhiles.qweather.presentation.screenmodel.PreferenceManager import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.Theme import com.henryhiles.qweather.presentation.screenmodel.Theme
import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class QWeatherActivity : ComponentActivity() { class QWeatherActivity : ComponentActivity() {
private val prefs: PreferenceManager by inject() private val prefs: AppearancePreferenceManager by inject()
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -3,6 +3,8 @@ package com.henryhiles.qweather.presentation.components.weather
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Thermostat
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -16,14 +18,12 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.R import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.WeatherDataDisplay import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
import com.henryhiles.qweather.presentation.screenmodel.WeatherState
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
@Composable @Composable
fun WeatherCard(state: WeatherState, modifier: Modifier = Modifier) { fun WeatherCard(state: HourlyWeatherState, modifier: Modifier = Modifier) {
state.weatherInfo?.currentWeatherData?.let { state.hourlyWeatherInfo?.currentWeatherData?.let {
val formattedTime = remember(it) { val formattedTime = remember(it) {
it.time.format(DateTimeFormatter.ofPattern("HH:mm")) it.time.format(DateTimeFormatter.ofPattern("HH:mm"))
} }
@ -48,7 +48,7 @@ fun WeatherCard(state: WeatherState, modifier: Modifier = Modifier) {
modifier = Modifier.width(200.dp) modifier = Modifier.width(200.dp)
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(text = "${it.temperatureCelsius}°C", fontSize = 50.sp) Text(text = "${it.temperature}°C", fontSize = 50.sp)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(text = it.weatherType.weatherDesc, fontSize = 20.sp) Text(text = it.weatherType.weatherDesc, fontSize = 20.sp)
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -57,19 +57,19 @@ fun WeatherCard(state: WeatherState, modifier: Modifier = Modifier) {
horizontalArrangement = Arrangement.SpaceAround horizontalArrangement = Arrangement.SpaceAround
) { ) {
WeatherDataDisplay( WeatherDataDisplay(
value = it.pressure.roundToInt(), value = it.apparentTemperature,
unit = "hpa", unit = "°C",
icon = ImageVector.vectorResource(id = R.drawable.ic_pressure), icon = Icons.Default.Thermostat,
description = "Pressure", description = "Feels like",
) )
WeatherDataDisplay( WeatherDataDisplay(
value = it.humidity.roundToInt(), value = it.precipitationProbability,
unit = "%", unit = "%",
icon = ImageVector.vectorResource(id = R.drawable.ic_drop), icon = ImageVector.vectorResource(id = R.drawable.ic_drop),
description = "Humidity" description = "Chance of precipitation"
) )
WeatherDataDisplay( WeatherDataDisplay(
value = it.windSpeed.roundToInt(), value = it.windSpeed,
unit = "km/h", unit = "km/h",
icon = ImageVector.vectorResource(id = R.drawable.ic_wind), icon = ImageVector.vectorResource(id = R.drawable.ic_wind),
description = "Wind Speed", description = "Wind Speed",

View file

@ -1,4 +1,4 @@
package com.henryhiles.qweather.presentation.components package com.henryhiles.qweather.presentation.components.weather
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.R
@Composable @Composable
fun WeatherDataDisplay( fun WeatherDataDisplay(
value: Int, value: Int?,
unit: String, unit: String,
icon: ImageVector, icon: ImageVector,
description: String, description: String,
@ -24,10 +26,9 @@ fun WeatherDataDisplay(
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = description, contentDescription = description,
// tint = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier.size(25.dp) modifier = Modifier.size(25.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = "$value$unit") Text(text = if (value == null) stringResource(id = R.string.unknown) else "$value$unit")
} }
} }

View file

@ -1,4 +1,4 @@
package com.henryhiles.qweather.presentation.components package com.henryhiles.qweather.presentation.components.weather
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@ -10,13 +10,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.presentation.components.weather.WeatherHour import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
import com.henryhiles.qweather.presentation.screenmodel.WeatherState
import java.time.LocalDateTime import java.time.LocalDateTime
@Composable @Composable
fun WeatherForecast(state: WeatherState, modifier: Modifier = Modifier) { fun WeatherForecast(state: HourlyWeatherState, modifier: Modifier = Modifier) {
state.weatherInfo?.weatherDataPerDay?.get(0)?.let { state.hourlyWeatherInfo?.weatherDataPerDay?.get(0)?.let {
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -25,7 +24,7 @@ fun WeatherForecast(state: WeatherState, modifier: Modifier = Modifier) {
Text(text = "Today", fontSize = 20.sp, color = Color.White) Text(text = "Today", fontSize = 20.sp, color = Color.White)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val rowState = rememberLazyListState(LocalDateTime.now().hour) val rowState = rememberLazyListState(LocalDateTime.now().hour)
LazyRow(state = rowState) { LazyRow(state = rowState) {
items(it) { items(it) {
WeatherHour( WeatherHour(

View file

@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.domain.weather.WeatherData import com.henryhiles.qweather.domain.weather.HourlyWeatherData
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Composable @Composable
fun WeatherHour(data: WeatherData, modifier: Modifier = Modifier) { fun WeatherHour(data: HourlyWeatherData, modifier: Modifier = Modifier) {
data.let { data.let {
val formattedTime = remember(it) { val formattedTime = remember(it) {
it.time.format(DateTimeFormatter.ofPattern("HH:mm")) it.time.format(DateTimeFormatter.ofPattern("HH:mm"))
@ -29,10 +29,10 @@ fun WeatherHour(data: WeatherData, modifier: Modifier = Modifier) {
Text(text = formattedTime) Text(text = formattedTime)
Image( Image(
painter = painterResource(id = it.weatherType.iconRes), painter = painterResource(id = it.weatherType.iconRes),
contentDescription = "Image of ${data.weatherType.weatherDesc}", contentDescription = "Image of ${it.weatherType.weatherDesc}",
modifier = Modifier.width(40.dp) modifier = Modifier.width(40.dp)
) )
Text(text = "${it.temperatureCelsius}°C") Text(text = "${it.temperature}°C")
} }
} }

View file

@ -16,7 +16,7 @@ import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.LargeToolbar import com.henryhiles.qweather.presentation.components.LargeToolbar
import com.henryhiles.qweather.presentation.components.settings.SettingsItemChoice import com.henryhiles.qweather.presentation.components.settings.SettingsItemChoice
import com.henryhiles.qweather.presentation.components.settings.SettingsSwitch import com.henryhiles.qweather.presentation.components.settings.SettingsSwitch
import com.henryhiles.qweather.presentation.screenmodel.AppearanceSettingsScreenModel import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
class AppearanceSettingsScreen : Screen { class AppearanceSettingsScreen : Screen {
@ -25,7 +25,7 @@ class AppearanceSettingsScreen : Screen {
@Composable @Composable
private fun Screen( private fun Screen(
screenModel: AppearanceSettingsScreenModel = getScreenModel() screenModel: AppearancePreferencesScreenModel = getScreenModel()
) { ) {
val ctx = LocalContext.current val ctx = LocalContext.current

View file

@ -7,7 +7,7 @@ import cafe.adriel.voyager.core.model.ScreenModel
import com.henryhiles.qweather.R import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.manager.BasePreferenceManager import com.henryhiles.qweather.domain.manager.BasePreferenceManager
class AppearanceSettingsManager(context: Context) : class AppearancePreferenceManager(context: Context) :
BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) { BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) {
var theme by enumPreference("theme", Theme.SYSTEM) var theme by enumPreference("theme", Theme.SYSTEM)
var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
@ -19,6 +19,6 @@ enum class Theme(@StringRes val label: Int) {
DARK(R.string.theme_dark); DARK(R.string.theme_dark);
} }
class AppearanceSettingsScreenModel( class AppearancePreferencesScreenModel(
val prefs: AppearanceSettingsManager val prefs: AppearancePreferenceManager
) : ScreenModel ) : ScreenModel

View file

@ -8,20 +8,20 @@ import cafe.adriel.voyager.core.model.coroutineScope
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
import com.henryhiles.qweather.domain.weather.WeatherInfo import com.henryhiles.qweather.domain.weather.DailyWeatherData
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class WeatherState( data class DailyWeatherState(
val weatherInfo: WeatherInfo? = null, val dailyWeatherData: List<DailyWeatherData>? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null
) )
class WeatherScreenModel constructor( class DailyWeatherScreenModel constructor(
private val repository: WeatherRepository, private val repository: WeatherRepository,
private val locationTracker: LocationTracker, private val locationTracker: LocationTracker,
) : ScreenModel { ) : ScreenModel {
var state by mutableStateOf(WeatherState()) var state by mutableStateOf(DailyWeatherState())
private set private set
fun loadWeatherInfo() { fun loadWeatherInfo() {
@ -30,10 +30,10 @@ class WeatherScreenModel constructor(
val currentLocation = locationTracker.getCurrentLocation() val currentLocation = locationTracker.getCurrentLocation()
currentLocation?.let { location -> currentLocation?.let { location ->
state = when (val result = state = when (val result =
repository.getWeatherData(location.latitude, location.longitude)) { repository.getDailyWeatherData(location.latitude, location.longitude)) {
is Resource.Success -> { is Resource.Success -> {
state.copy( state.copy(
weatherInfo = result.data, dailyWeatherData = result.data,
isLoading = false, isLoading = false,
error = null error = null
) )
@ -41,7 +41,7 @@ class WeatherScreenModel constructor(
is Resource.Error -> { is Resource.Error -> {
state.copy( state.copy(
weatherInfo = null, dailyWeatherData = null,
isLoading = false, isLoading = false,
error = result.message error = result.message
) )

View file

@ -0,0 +1,58 @@
package com.henryhiles.qweather.presentation.screenmodel
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.domain.location.LocationTracker
import com.henryhiles.qweather.domain.repository.WeatherRepository
import com.henryhiles.qweather.domain.util.Resource
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
import kotlinx.coroutines.launch
data class HourlyWeatherState(
val hourlyWeatherInfo: HourlyWeatherInfo? = null,
val isLoading: Boolean = false,
val error: String? = null
)
class HourlyWeatherScreenModel constructor(
private val repository: WeatherRepository,
private val locationTracker: LocationTracker,
) : ScreenModel {
var state by mutableStateOf(HourlyWeatherState())
private set
fun loadWeatherInfo() {
coroutineScope.launch {
state = state.copy(isLoading = true, error = null)
val currentLocation = locationTracker.getCurrentLocation()
currentLocation?.let { location ->
state = when (val result =
repository.getHourlyWeatherData(location.latitude, location.longitude)) {
is Resource.Success -> {
state.copy(
hourlyWeatherInfo = result.data,
isLoading = false,
error = null
)
}
is Resource.Error -> {
state.copy(
hourlyWeatherInfo = null,
isLoading = false,
error = result.message
)
}
}
} ?: kotlin.run {
state = state.copy(
isLoading = false,
error = "Couldn't retrieve location. Make sure to grant permission and enable GPS."
)
}
}
}
}

View file

@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.*
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.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -21,8 +24,8 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.henryhiles.qweather.R import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.weather.WeatherCard import com.henryhiles.qweather.presentation.components.weather.WeatherCard
import com.henryhiles.qweather.presentation.components.WeatherForecast import com.henryhiles.qweather.presentation.components.weather.WeatherForecast
import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
object TodayTab : Tab { object TodayTab : Tab {
override val options: TabOptions override val options: TabOptions
@ -43,7 +46,7 @@ object TodayTab : Tab {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable @Composable
override fun Content() { override fun Content() {
val weatherViewModel = getScreenModel<WeatherScreenModel>() val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val permissionsState = rememberPermissionState( val permissionsState = rememberPermissionState(
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION,
@ -65,27 +68,16 @@ object TodayTab : Tab {
) )
} }
weatherViewModel.state.error != null -> { weatherViewModel.state.error != null -> {
AlertDialog(onDismissRequest = {}) { AlertDialog(
Surface( onDismissRequest = {},
shape = MaterialTheme.shapes.large confirmButton = {},
) { title = { Text(text = "An error occurred") }, text = {
Column(modifier = Modifier.padding(16.dp)) { SelectionContainer {
Text( Text(
text = "An error occurred", text = weatherViewModel.state.error!!,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(Alignment.CenterHorizontally)
) )
SelectionContainer {
Text(
text = weatherViewModel.state.error!!,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
} }
} })
}
} }
else -> { else -> {
Column( Column(

View file

@ -1,15 +1,30 @@
package com.henryhiles.qweather.presentation.tabs package com.henryhiles.qweather.presentation.tabs
import android.Manifest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
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.material3.Text import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.henryhiles.qweather.R import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
object WeekTab : Tab { object WeekTab : Tab {
override val options: TabOptions override val options: TabOptions
@ -27,8 +42,91 @@ object WeekTab : Tab {
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
override fun Content() { override fun Content() {
Text(text = "Week Screen") val weatherViewModel = getScreenModel<DailyWeatherScreenModel>()
val permissionsState = rememberPermissionState(
Manifest.permission.ACCESS_FINE_LOCATION,
) {
weatherViewModel.loadWeatherInfo()
}
LaunchedEffect(key1 = true) {
permissionsState.launchPermissionRequest()
}
Box(modifier = Modifier.fillMaxSize()) {
when {
weatherViewModel.state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(
Alignment.Center
)
)
}
weatherViewModel.state.error != null -> {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = { Text(text = "An error occurred") },
text = {
SelectionContainer {
Text(
text = weatherViewModel.state.error!!,
)
}
})
}
else -> {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
weatherViewModel.state.dailyWeatherData?.let { data ->
items(data) {
Card(
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = it.weatherType.iconRes),
contentDescription = "Image of ${it.weatherType}",
modifier = Modifier.width(48.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = //it.date.dayOfWeek
when (it.date.dayOfWeek.value) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
7 -> "Sunday"
else -> "Unknown"
}
)
Spacer(modifier = Modifier.width(24.dp))
Text(text = it.temperatureMax.toString())
}
}
}
}
}
}
}
}
} }
} }

View file

@ -17,4 +17,6 @@
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="unknown">Unknown</string>
</resources> </resources>