not working

This commit is contained in:
Henry Hiles 2023-04-16 16:28:40 -04:00
parent a13db7aa7d
commit 376f28dc9d
21 changed files with 360 additions and 57 deletions

View file

@ -3,6 +3,7 @@ package com.henryhiles.qweather.di
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import com.henryhiles.qweather.domain.remote.GeocodingApi
import com.henryhiles.qweather.domain.remote.WeatherApi
import okhttp3.Cache
import okhttp3.Interceptor
@ -30,7 +31,6 @@ private fun isNetworkAvailable(context: Context): Boolean {
val appModule = module {
fun provideWeatherApi(context: Context): WeatherApi {
val cacheControlInterceptor = Interceptor { chain ->
val originalResponse = chain.proceed(chain.request())
if (isNetworkAvailable(context)) {
@ -50,13 +50,25 @@ val appModule = module {
val cache = Cache(context.cacheDir, cacheSize.toLong())
val builder = Builder()
.cache(cache)
builder.networkInterceptors()
.add(cacheControlInterceptor)
builder.networkInterceptors().add(cacheControlInterceptor)
val okHttpClient = builder.build()
return Retrofit.Builder().baseUrl("https://api.open-meteo.com").client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create()).build().create()
return Retrofit.Builder()
.baseUrl("https://api.open-meteo.com")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create()
}
fun provideGeocodingApi(): GeocodingApi {
return Retrofit.Builder()
.baseUrl("https://geocoding-api.open-meteo.com")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create()
}
singleOf(::provideWeatherApi)
singleOf(::provideGeocodingApi)
}

View file

@ -1,9 +1,12 @@
package com.henryhiles.qweather.di
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val managerModule = module {
singleOf(::AppearancePreferenceManager)
singleOf(::LocationPreferenceManager)
}

View file

@ -1,9 +1,11 @@
package com.henryhiles.qweather.di
import com.henryhiles.qweather.domain.repository.GeocodingRepository
import com.henryhiles.qweather.domain.repository.WeatherRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::WeatherRepository)
singleOf(::GeocodingRepository)
}

View file

@ -3,11 +3,13 @@ package com.henryhiles.qweather.di
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val screenModelModule = module {
factoryOf(::AppearancePreferencesScreenModel)
factoryOf(::LocationPickerScreenModel)
factoryOf(::HourlyWeatherScreenModel)
factoryOf(::DailyWeatherScreenModel)
}

View file

@ -8,7 +8,7 @@ import android.location.Location
import android.location.LocationManager
import androidx.core.content.ContextCompat
class LocationTracker constructor(
class LocationTracker(
private val application: Application
) {
fun getCurrentLocation(): Location? {

View file

@ -10,15 +10,15 @@ data class DailyWeatherDataDto(
@field:Json(name = "precipitation_probability_max")
val precipitationProbabilityMax: List<Int>,
@field:Json(name = "precipitation_sum")
val precipitationSum: List<Double>,
val precipitationSum: List<Float>,
@field:Json(name = "windspeed_10m_max")
val windSpeedMax: List<Double>,
val windSpeedMax: List<Float>,
@field:Json(name = "temperature_2m_max")
val temperatureMax: List<Double>,
val temperatureMax: List<Float>,
@field:Json(name = "temperature_2m_min")
val temperatureMin: List<Double>,
val temperatureMin: List<Float>,
@field:Json(name = "apparent_temperature_max")
val apparentTemperatureMax: List<Double>,
val apparentTemperatureMax: List<Float>,
@field:Json(name = "apparent_temperature_min")
val apparentTemperatureMin: List<Double>
val apparentTemperatureMin: List<Float>
)

View file

@ -0,0 +1,11 @@
package com.henryhiles.qweather.domain.remote
import retrofit2.http.GET
import retrofit2.http.Query
interface GeocodingApi {
@GET("v1/search?count=4")
suspend fun getGeocodingData(
@Query("name") location: String,
): GeocodingDto
}

View file

@ -0,0 +1,8 @@
package com.henryhiles.qweather.domain.remote
import com.squareup.moshi.Json
data class GeocodingDto(
@field:Json(name = "results")
val results: List<GeocodingLocationDto>
)

View file

@ -0,0 +1,16 @@
package com.henryhiles.qweather.domain.remote
import com.squareup.moshi.Json
data class GeocodingLocationDto(
@field:Json(name = "name")
val city: String,
@field:Json(name = "country")
val country: String,
@field:Json(name = "admin1")
val admin: String,
@field:Json(name = "latitude")
val latitude: Float,
@field:Json(name = "longitude")
val longitude: Float
)

View file

@ -6,13 +6,13 @@ data class HourlyWeatherDataDto(
@field:Json(name = "time")
val time: List<String>,
@field:Json(name = "temperature_2m")
val temperature: List<Double>,
val temperature: List<Float>,
@field:Json(name = "apparent_temperature")
val apparentTemperature: List<Double>,
val apparentTemperature: List<Float>,
@field:Json(name = "weathercode")
val weatherCode: List<Int>,
@field:Json(name = "precipitation_probability")
val precipitationProbability: List<Int>,
@field:Json(name = "windspeed_10m")
val windSpeed: List<Double>,
val windSpeed: List<Float>,
)

View file

@ -15,14 +15,14 @@ const val URL = "v1/forecast?$HOURLY&$DAILY&$TIMEZONE&$FORECAST_DAYS"
interface WeatherApi {
@GET(URL)
suspend fun getWeatherData(
@Query("latitude") lat: Double,
@Query("longitude") long: Double,
@Query("latitude") lat: Float,
@Query("longitude") long: Float,
): WeatherDto
@Headers("Cache-Control: no-cache")
@GET(URL)
suspend fun getWeatherDataWithoutCache(
@Query("latitude") lat: Double,
@Query("longitude") long: Double,
@Query("latitude") lat: Float,
@Query("longitude") long: Float,
): WeatherDto
}

View file

@ -0,0 +1,18 @@
package com.henryhiles.qweather.domain.repository
import com.henryhiles.qweather.domain.remote.GeocodingApi
import com.henryhiles.qweather.domain.remote.GeocodingLocationDto
import com.henryhiles.qweather.domain.util.Resource
class GeocodingRepository(private val api: GeocodingApi) {
suspend fun getGeocodingData(location: String): Resource<List<GeocodingLocationDto>> {
return try {
Resource.Success(
data = api.getGeocodingData(location = location).results
)
} catch (e: Exception) {
e.printStackTrace()
Resource.Error(e.message ?: "An unknown error occurred.")
}
}
}

View file

@ -7,10 +7,10 @@ import com.henryhiles.qweather.domain.util.Resource
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
class WeatherRepository constructor(private val api: WeatherApi) {
class WeatherRepository(private val api: WeatherApi) {
suspend fun getHourlyWeatherData(
lat: Double,
long: Double,
lat: Float,
long: Float,
cache: Boolean = true
): Resource<HourlyWeatherInfo> {
return try {
@ -31,8 +31,8 @@ class WeatherRepository constructor(private val api: WeatherApi) {
}
suspend fun getDailyWeatherData(
lat: Double,
long: Double,
lat: Float,
long: Float,
cache: Boolean = true
): Resource<List<DailyWeatherData>> {
return try {
@ -43,8 +43,7 @@ class WeatherRepository constructor(private val api: WeatherApi) {
) else api.getWeatherDataWithoutCache(
lat = lat,
long = long
))
.dailyWeatherData.toDailyWeatherDataMap()
)).dailyWeatherData.toDailyWeatherDataMap()
)
} catch (e: Exception) {
e.printStackTrace()

View file

@ -5,17 +5,23 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.SlideTransition
import com.henryhiles.qweather.presentation.screen.LocationPickerScreen
import com.henryhiles.qweather.presentation.screen.MainScreen
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.Theme
import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme
import org.koin.android.ext.android.inject
class QWeatherActivity : ComponentActivity() {
private val prefs: AppearancePreferenceManager by inject()
private val location: LocationPreferenceManager by inject()
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@ -26,10 +32,12 @@ class QWeatherActivity : ComponentActivity() {
Theme.LIGHT -> false
Theme.DARK -> true
}
val isLocationSet = location.location != ""
WeatherAppTheme(darkTheme = isDark, monet = prefs.monet) {
Surface {
Navigator(screen = MainScreen()) {
Surface(modifier = Modifier.fillMaxSize()) {
Text(text = location.location)
Navigator(screen = if (isLocationSet) MainScreen() else LocationPickerScreen()) {
SlideTransition(it)
}
}

View file

@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
@ -21,8 +23,8 @@ fun WeatherHour(
onChangeSelected: () -> Unit
) {
data.let {
val formattedTime = remember(it) {
it.time.format(DateTimeFormatter.ofPattern("HH:mm"))
val formattedTime by remember {
derivedStateOf { it.time.format(DateTimeFormatter.ofPattern("HH:mm")) }
}
Card(modifier = modifier.clickable {
onChangeSelected()
@ -44,21 +46,7 @@ fun WeatherHour(
Text(text = "${it.temperature}°C")
}
}
// Column(
// horizontalAlignment = Alignment.CenterHorizontally,
// verticalArrangement = Arrangement.SpaceBetween,
// modifier = modifier
// ) {
// Text(text = formattedTime)
// Image(
// painter = painterResource(id = it.weatherType.iconRes),
// contentDescription = "Image of ${it.weatherType.weatherDesc}",
// modifier = Modifier.width(40.dp)
// )
// Text(text = "${it.temperature}°C")
// }
}
}

View file

@ -20,14 +20,10 @@ import com.henryhiles.qweather.presentation.components.settings.SettingsSwitch
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
class AppearanceSettingsScreen : Screen {
@Composable
override fun Content() = Screen()
@Composable
private fun Screen() {
override fun Content() {
val screenModel: AppearancePreferencesScreenModel = getScreenModel()
val ctx = LocalContext.current
val context = LocalContext.current
Scaffold(topBar = {
SmallToolbar(
@ -51,7 +47,7 @@ class AppearanceSettingsScreen : Screen {
SettingsItemChoice(
label = stringResource(R.string.appearance_theme),
pref = screenModel.prefs.theme,
labelFactory = { ctx.getString(it.label) }
labelFactory = { context.getString(it.label) }
) { screenModel.prefs.theme = it }
}

View file

@ -0,0 +1,167 @@
package com.henryhiles.qweather.presentation.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel
class LocationPickerScreen : Screen {
@Composable
override fun Content() {
val screenModel: LocationPickerScreenModel = getScreenModel()
var latitude by remember { mutableStateOf(0f) }
var longitude by remember { mutableStateOf(0f) }
var location by remember { mutableStateOf("") }
var locationSearch by remember { mutableStateOf("") }
val navigator = LocalNavigator.current
val context = LocalContext.current
Box(modifier = Modifier.fillMaxSize()) {
screenModel.state.error?.let {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = { Text(text = stringResource(id = R.string.error)) },
text = {
SelectionContainer {
Text(
text = it,
)
}
},
)
} ?: AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(
onClick = {
screenModel.prefs.location = location
screenModel.prefs.latitude = latitude
screenModel.prefs.longitude = longitude
navigator?.push(MainScreen())
},
enabled = location != ""
) {
Text(text = stringResource(id = R.string.action_apply))
}
},
title = { Text(text = stringResource(id = R.string.location_choose)) },
text = {
Column {
OutlinedTextField(
label = { Text(text = stringResource(id = R.string.location)) },
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(onSearch = {
screenModel.loadGeolocationInfo(
locationSearch
)
}),
maxLines = 1,
value = locationSearch,
onValueChange = {
locationSearch = it
},
trailingIcon = {
if (locationSearch == "")
IconButton(onClick = {
}) {
Icon(
imageVector = Icons.Outlined.MyLocation,
contentDescription = stringResource(id = R.string.location_auto_pick)
)
}
else
IconButton(onClick = {
screenModel.loadGeolocationInfo(
locationSearch
)
}) {
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = stringResource(id = R.string.action_search)
)
}
}
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "${screenModel.state.locations != null}")
screenModel.state.locations?.let {
Text(
text = "hi"
)
}
if (screenModel.state.isLoading) CircularProgressIndicator(
modifier = Modifier.align(
Alignment.CenterHorizontally
)
) else screenModel.state.locations?.let {
LazyColumn {
items(it) {
val locationText by remember {
mutableStateOf(
context.getString(
R.string.location_string,
it.city, it.admin, it.country
)
)
}
Spacer(modifier = Modifier.height(8.dp))
Card(modifier = Modifier.clickable {
location = locationText
longitude = it.longitude
latitude = it.latitude
}) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (location == locationText) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(
id = R.string.selected
),
modifier = Modifier.height(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(text = locationText)
}
}
}
}
}
}
}
)
}
}
}

View file

@ -19,7 +19,7 @@ data class DailyWeatherState(
val expanded: Int? = null
)
class DailyWeatherScreenModel constructor(
class DailyWeatherScreenModel(
private val repository: WeatherRepository,
private val locationTracker: LocationTracker,
) : ScreenModel {
@ -34,8 +34,8 @@ class DailyWeatherScreenModel constructor(
currentLocation?.let { location ->
state = when (val result =
repository.getDailyWeatherData(
lat = location.latitude,
long = location.longitude,
lat = location.latitude.toFloat(),
long = location.longitude.toFloat(),
cache = cache
)) {
is Resource.Success -> {

View file

@ -20,7 +20,7 @@ data class HourlyWeatherState(
val selected: Int? = null
)
class HourlyWeatherScreenModel constructor(
class HourlyWeatherScreenModel(
private val repository: WeatherRepository,
private val locationTracker: LocationTracker,
private val context: Context
@ -35,8 +35,8 @@ class HourlyWeatherScreenModel constructor(
currentLocation?.let { location ->
state = when (val result =
repository.getHourlyWeatherData(
lat = location.latitude,
long = location.longitude,
lat = location.latitude.toFloat(),
long = location.longitude.toFloat(),
cache = cache
)) {
is Resource.Success -> {

View file

@ -0,0 +1,64 @@
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.domain.location.LocationTracker
import com.henryhiles.qweather.domain.manager.BasePreferenceManager
import com.henryhiles.qweather.domain.remote.GeocodingLocationDto
import com.henryhiles.qweather.domain.repository.GeocodingRepository
import com.henryhiles.qweather.domain.util.Resource
import kotlinx.coroutines.launch
data class LocationPickerState(
val locations: List<GeocodingLocationDto>? = null,
val isLoading: Boolean = false,
val error: String? = null,
)
class LocationPreferenceManager(context: Context) :
BasePreferenceManager(context.getSharedPreferences("location", Context.MODE_PRIVATE)) {
var latitude by floatPreference("lat", 0f)
var longitude by floatPreference("long", 0f)
var location by stringPreference("string")
}
class LocationPickerScreenModel(
val prefs: LocationPreferenceManager,
private val repository: GeocodingRepository,
private val locationTracker: LocationTracker,
private val context: Context
) : ScreenModel {
var state by mutableStateOf(LocationPickerState())
private set
fun loadGeolocationInfo(location: String) {
coroutineScope.launch {
state = state.copy(isLoading = true, error = null)
state = when (val result =
repository.getGeocodingData(
location = location
)) {
is Resource.Success -> {
state.copy(
locations = result.data,
isLoading = false,
error = null,
)
}
is Resource.Error -> {
state.copy(
locations = null,
isLoading = false,
error = result.message
)
}
}
}
}
}

View file

@ -5,11 +5,15 @@
<string name="tab_settings">Settings</string>
<string name="action_back">Back</string>
<string name="action_apply">Apply</string>
<string name="action_confirm">Confirm</string>
<string name="action_open_about">About</string>
<string name="action_search">Search</string>
<string name="action_reload">Reload</string>
<string name="action_try_again">Try Again</string>
<string name="selected">Selected</string>
<string name="appearance_theme">Theme</string>
<string name="appearance_monet">Dynamic Theme</string>
<string name="appearance_monet_description">Available on Android 12+</string>
@ -17,6 +21,11 @@
<string name="settings_appearance">Appearance</string>
<string name="settings_appearance_description">Theme, code style</string>
<string name="location">Location</string>
<string name="location_string">%1$s, %2$s, %3$s</string>
<string name="location_auto_pick">Auto-pick location</string>
<string name="location_choose">Choose a Location</string>
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>