settings frontend done, just need to setup screenmodel

This commit is contained in:
Henry Hiles 2023-03-30 12:32:09 -04:00
parent e132ceb55f
commit 0809535e16
30 changed files with 732 additions and 143 deletions

View file

@ -57,25 +57,25 @@ android {
}
dependencies {
val composeVersion = "1.4.0"
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.compose.ui:ui:1.4.0")
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("androidx.compose.material3:material3:1.1.0-beta01")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.0")
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.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.4.0")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.0")
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"
implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-androidx:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
@ -92,5 +92,7 @@ dependencies {
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
// Accompanist
val accompanistVersion = "0.30.0"
implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
}

View file

@ -14,11 +14,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.WeatherApp">
<activity
android:name=".presentation.activity.MainActivity"
android:name=".presentation.QWeatherActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View file

@ -18,11 +18,6 @@ class DefaultLocationTracker constructor(
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
application,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val locationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val isGpsEnabled = locationManager.isProviderEnabled(

View file

@ -15,7 +15,7 @@ fun WeatherDataDto.toWeatherDataMap(): Map<Int, List<WeatherData>> {
IndexedWeatherData(
index = index, data = WeatherData(
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
temperatureCelsius = temperatures[index],
temperatureCelsius = temperatures[index].toInt(),
pressure = pressures[index],
windSpeed = windSpeeds[index],
humidity = humidities[index],

View file

@ -1,8 +1,8 @@
package com.henryhiles.qweather.di
import com.henryhiles.qweather.data.remote.WeatherApi
import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import retrofit2.Retrofit
@ -36,5 +36,7 @@ val appModule = module {
// single {
// LocationServices.getFusedLocationProviderClient(get<Application>())
// }
viewModelOf(::WeatherViewModel)
factoryOf(::WeatherScreenModel)
// factory { WeatherScreenModel(get(), get()) }
}

View file

@ -4,7 +4,7 @@ import java.time.LocalDateTime
data class WeatherData(
val time: LocalDateTime,
val temperatureCelsius: Double,
val temperatureCelsius: Int,
val pressure: Double,
val windSpeed: Double,
val humidity: Double,

View file

@ -0,0 +1,27 @@
package com.henryhiles.qweather.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material3.Surface
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.SlideTransition
import com.henryhiles.qweather.presentation.screen.MainScreen
import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme
class QWeatherActivity : ComponentActivity() {
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WeatherAppTheme {
Surface {
Navigator(screen = MainScreen()) {
SlideTransition(it)
}
}
}
}
}
}

View file

@ -1,49 +0,0 @@
package com.henryhiles.qweather.presentation.activity
import android.Manifest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
import com.henryhiles.qweather.presentation.screen.TodayScreen
import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme
import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel
class MainActivity : ComponentActivity() {
private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val weatherViewModel = getViewModel<WeatherViewModel>()
permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { weatherViewModel.loadWeatherInfo() }
permissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
)
)
setContent {
WeatherAppTheme {
Surface {
Navigator(TodayScreen()) { navigator ->
Scaffold(
topBar = { /* ... */ },
content = { padding -> Box(modifier = Modifier.padding(padding)) { CurrentScreen() } },
bottomBar = { /* ... */ }
)
}
}
}
}
}
}

View file

@ -0,0 +1,21 @@
package com.henryhiles.qweather.presentation.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import com.henryhiles.qweather.R
@Composable
fun BackButton() {
val nav = LocalNavigator.current
if (nav?.canPop == true) {
IconButton(onClick = { nav.pop() }) {
Icon(Icons.Filled.ArrowBack, stringResource(R.string.action_back))
}
}
}

View file

@ -0,0 +1,18 @@
package com.henryhiles.qweather.presentation.components
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun LargeToolbar(
title: String,
actions: @Composable RowScope.() -> Unit = {},
) {
LargeTopAppBar(
title = { Text(text = title) },
navigationIcon = { BackButton() },
actions = actions,
)
}

View file

@ -0,0 +1,47 @@
package com.henryhiles.qweather.presentation.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@Composable
inline fun <reified E : Enum<E>> EnumRadioController(
default: E,
labelFactory: (E) -> String = { it.toString() },
crossinline onChoiceSelected: (E) -> Unit
) {
var choice by remember { mutableStateOf(default) }
val ctx = LocalContext.current
Column {
enumValues<E>().forEach {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
choice = it
onChoiceSelected(it)
},
verticalAlignment = Alignment.CenterVertically
) {
Text(labelFactory(it))
Spacer(Modifier.weight(1f))
RadioButton(
selected = it == choice,
onClick = {
choice = it
onChoiceSelected(it)
})
}
}
}
}

View file

@ -1,2 +0,0 @@
package com.henryhiles.qweather.presentation.components

View file

@ -0,0 +1,35 @@
package com.henryhiles.qweather.presentation.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
@Composable
fun SettingsCategory(
icon: ImageVector,
text: String,
subtext: String,
destination: (() -> Screen)? = null
) {
val screen = destination?.invoke()
val nav = LocalNavigator.current
Box(
modifier = Modifier
.clickable {
screen?.let { nav?.push(it) }
}
) {
SettingItem(
icon = { Icon(icon, null) },
text = { Text(text) },
secondaryText = { Text(subtext) }
)
}
}

View file

@ -0,0 +1,50 @@
package com.henryhiles.qweather.presentation.components.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.EnumRadioController
@Composable
inline fun <reified E : Enum<E>> SettingsChoiceDialog(
visible: Boolean = false,
default: E,
noinline title: @Composable () -> Unit,
crossinline labelFactory: (E) -> String = { it.toString() },
noinline onRequestClose: () -> Unit = {},
crossinline description: @Composable () -> Unit = {},
noinline onChoice: (E) -> Unit = {},
) {
var choice by remember { mutableStateOf(default) }
AnimatedVisibility(
visible = visible,
enter = slideInVertically(),
exit = slideOutVertically()
) {
AlertDialog(
onDismissRequest = { onRequestClose() },
title = title,
text = {
description()
EnumRadioController(
default,
labelFactory
) { choice = it }
},
confirmButton = {
FilledTonalButton(onClick = { onChoice(choice) }) {
Text(text = stringResource(id = R.string.action_confirm))
}
}
)
}
}

View file

@ -0,0 +1,58 @@
package com.henryhiles.qweather.presentation.components.settings
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SettingItem(
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
text: @Composable () -> Unit,
secondaryText: @Composable (() -> Unit) = { },
trailing: @Composable (() -> Unit) = { },
) {
Row(
modifier = modifier
.heightIn(min = 64.dp)
.fillMaxWidth()
.padding(horizontal = 18.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) Box(modifier = Modifier.padding(8.dp)) {
icon()
}
Column(
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
ProvideTextStyle(
MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Normal,
fontSize = 19.sp
)
) {
text()
}
ProvideTextStyle(
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
)
) {
secondaryText()
}
}
Spacer(Modifier.weight(1f, true))
trailing()
}
}

View file

@ -0,0 +1,44 @@
package com.henryhiles.qweather.presentation.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@Composable
inline fun <reified E : Enum<E>> SettingsItemChoice(
label: String,
title: String = label,
disabled: Boolean = false,
pref: E,
crossinline labelFactory: (E) -> String = { it.toString() },
crossinline onPrefChange: (E) -> Unit,
) {
val choiceLabel = labelFactory(pref)
var opened by remember {
mutableStateOf(false)
}
SettingItem(
modifier = Modifier.clickable { opened = true },
text = { Text(text = label) },
) {
SettingsChoiceDialog(
visible = opened,
title = { Text(title) },
default = pref,
labelFactory = labelFactory,
onRequestClose = {
opened = false
},
onChoice = {
opened = false
onPrefChange(it)
}
)
FilledTonalButton(onClick = { opened = true }, enabled = !disabled) {
Text(choiceLabel)
}
}
}

View file

@ -0,0 +1,32 @@
package com.henryhiles.qweather.presentation.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun SettingsSwitch(
label: String,
secondaryLabel: String? = null,
disabled: Boolean = false,
pref: Boolean,
onPrefChange: (Boolean) -> Unit,
) {
SettingItem(
modifier = Modifier.clickable(enabled = !disabled) { onPrefChange(!pref) },
text = { Text(text = label) },
secondaryText = {
secondaryLabel?.let {
Text(text = it)
}
}
) {
Switch(
checked = pref,
enabled = !disabled,
onCheckedChange = { onPrefChange(!pref) }
)
}
}

View file

@ -16,7 +16,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.viewmodel.WeatherState
import com.henryhiles.qweather.presentation.screenmodel.WeatherState
import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
@ -26,7 +26,6 @@ fun WeatherCard(state: WeatherState, modifier: Modifier = Modifier) {
val formattedTime = remember(it) {
it.time.format(DateTimeFormatter.ofPattern("HH:mm"))
}
Card(
shape = RoundedCornerShape(8.dp),
modifier = modifier.padding(16.dp)

View file

@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.presentation.viewmodel.WeatherState
import com.henryhiles.qweather.presentation.screenmodel.WeatherState
import java.time.LocalDateTime
@Composable

View file

@ -0,0 +1,65 @@
package com.henryhiles.qweather.presentation.screen
import android.os.Build
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.LargeToolbar
import com.henryhiles.qweather.presentation.components.settings.SettingsItemChoice
import com.henryhiles.qweather.presentation.components.settings.SettingsSwitch
import com.henryhiles.qweather.presentation.screenmodel.AppearanceSettingsScreenModel
class AppearanceSettingsScreen : Screen {
@Composable
override fun Content() = Screen()
@Composable
private fun Screen(
screenModel: AppearanceSettingsScreenModel = getScreenModel()
) {
val ctx = LocalContext.current
Scaffold(topBar = { Toolbar() }) { pv ->
Column(
modifier = Modifier
.padding(pv)
.verticalScroll(rememberScrollState())
) {
SettingsSwitch(
label = stringResource(R.string.appearance_monet),
secondaryLabel = stringResource(R.string.appearance_monet_description),
pref = screenModel.prefs.monet,
disabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) { screenModel.prefs.monet = it }
SettingsItemChoice(
label = stringResource(R.string.appearance_theme),
pref = screenModel.prefs.theme,
labelFactory = { ctx.getString(it.label) }
) { screenModel.prefs.theme = it }
}
}
}
@Composable
private fun Toolbar(
) {
LargeToolbar(
title = stringResource(R.string.settings_appearance),
)
}
}

View file

@ -0,0 +1,67 @@
package com.henryhiles.qweather.presentation.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.tab.TabNavigator
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.tabs.SettingsTab
import com.henryhiles.qweather.presentation.tabs.TodayTab
import com.henryhiles.qweather.presentation.tabs.WeekTab
class MainScreen : Screen {
@Composable
override fun Content() {
TabNavigator(tab = TodayTab) { navigator ->
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = navigator.current == TodayTab,
onClick = { navigator.current = TodayTab },
label = { Text(text = stringResource(id = R.string.tab_today)) },
icon = {
Icon(
imageVector = Icons.Default.Home,
contentDescription = stringResource(id = R.string.tab_today)
)
})
NavigationBarItem(
selected = navigator.current == WeekTab,
onClick = { navigator.current = WeekTab },
label = { Text(text = "Weekly") },
icon = {
Icon(
imageVector = Icons.Default.DateRange,
contentDescription = "Weekly"
)
})
NavigationBarItem(
selected = navigator.current == SettingsTab,
onClick = { navigator.current = SettingsTab },
label = { Text(text = "Settings") },
icon = {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings"
)
})
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
CurrentScreen()
}
}
}
}
}

View file

@ -1,61 +0,0 @@
package com.henryhiles.qweather.presentation.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.androidx.AndroidScreen
import com.henryhiles.qweather.presentation.components.WeatherCard
import com.henryhiles.qweather.presentation.components.WeatherForecast
import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel
import org.koin.androidx.compose.getViewModel
class TodayScreen : AndroidScreen() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val weatherViewModel = getViewModel<WeatherViewModel>()
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
) {
WeatherCard(state = weatherViewModel.state)
Spacer(modifier = Modifier.height(16.dp))
WeatherForecast(state = weatherViewModel.state)
}
if (weatherViewModel.state.isLoading) CircularProgressIndicator(
modifier = Modifier.align(
Alignment.Center
)
)
weatherViewModel.state.error?.let {
AlertDialog(onDismissRequest = {}) {
Surface(
shape = MaterialTheme.shapes.large
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "An error occurred",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
SelectionContainer {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,20 @@
package com.henryhiles.qweather.presentation.screenmodel
import android.content.Context
import android.os.Build
import androidx.annotation.StringRes
import cafe.adriel.voyager.core.model.ScreenModel
import com.henryhiles.qweather.R
class AppearanceSettingsScreenModel(context: Context) : ScreenModel {
var theme by enumPreference("theme", Theme.SYSTEM)
var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
}
enum class Theme(@StringRes val label: Int) {
SYSTEM(R.string.theme_system),
LIGHT(R.string.theme_light),
DARK(R.string.theme_dark);
}

View file

@ -1,10 +1,10 @@
package com.henryhiles.qweather.presentation.viewmodel
package com.henryhiles.qweather.presentation.screenmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
@ -17,15 +17,15 @@ data class WeatherState(
val error: String? = null
)
class WeatherViewModel constructor(
class WeatherScreenModel constructor(
private val repository: WeatherRepository,
private val locationTracker: LocationTracker,
) : ViewModel() {
) : ScreenModel {
var state by mutableStateOf(WeatherState())
private set
fun loadWeatherInfo() {
viewModelScope.launch {
coroutineScope.launch {
state = state.copy(isLoading = true, error = null)
val currentLocation = locationTracker.getCurrentLocation()
currentLocation?.let { location ->

View file

@ -0,0 +1,66 @@
package com.henryhiles.qweather.presentation.tabs
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.LargeToolbar
import com.henryhiles.qweather.presentation.components.settings.SettingsCategory
import com.henryhiles.qweather.presentation.screen.AppearanceSettingsScreen
object SettingsTab : Tab {
override val options: TabOptions
@Composable
get() {
val title = stringResource(R.string.tab_settings)
val icon = rememberVectorPainter(Icons.Default.Settings)
return remember {
TabOptions(
index = 0u,
title = title,
icon = icon
)
}
}
@Composable
override fun Content() {
Scaffold(
topBar = { Toolbar() },
) {
Column(
modifier = Modifier
.padding(it)
.verticalScroll(rememberScrollState())
) {
SettingsCategory(
icon = Icons.Outlined.Palette,
text = stringResource(R.string.settings_appearance),
subtext = stringResource(R.string.settings_appearance_description),
destination = ::AppearanceSettingsScreen
)
}
}
}
@Composable
private fun Toolbar() {
LargeToolbar(
title = stringResource(R.string.tab_settings),
)
}
}

View file

@ -0,0 +1,103 @@
package com.henryhiles.qweather.presentation.tabs
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.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.TabOptions
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.WeatherCard
import com.henryhiles.qweather.presentation.components.WeatherForecast
import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel
object TodayTab : Tab {
override val options: TabOptions
@Composable
get() {
val title = stringResource(R.string.tab_today)
val icon = rememberVectorPainter(Icons.Default.Home)
return remember {
TabOptions(
index = 0u,
title = title,
icon = icon
)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
override fun Content() {
val weatherViewModel = getScreenModel<WeatherScreenModel>()
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 = {}) {
Surface(
shape = MaterialTheme.shapes.large
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "An error occurred",
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 -> {
Column(
modifier = Modifier
.fillMaxSize()
) {
WeatherCard(state = weatherViewModel.state)
Spacer(modifier = Modifier.height(16.dp))
WeatherForecast(state = weatherViewModel.state)
}
}
}
}
}
}

View file

@ -0,0 +1,34 @@
package com.henryhiles.qweather.presentation.tabs
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.henryhiles.qweather.R
object WeekTab : Tab {
override val options: TabOptions
@Composable
get() {
val title = stringResource(R.string.tab_weekly)
val icon = rememberVectorPainter(Icons.Default.DateRange)
return remember {
TabOptions(
index = 0u,
title = title,
icon = icon
)
}
}
@Composable
override fun Content() {
Text(text = "Week Screen")
}
}

View file

@ -1,3 +1,20 @@
<resources>
<string name="app_name">QWeather</string>
<string name="app_name" translatable="false">QWeather</string>
<string name="tab_today">Today</string>
<string name="tab_weekly">Weekly</string>
<string name="tab_settings">Settings</string>
<string name="action_back">Back</string>
<string name="action_confirm">Confirm</string>
<string name="appearance_theme">Theme</string>
<string name="appearance_monet">Dynamic Theme</string>
<string name="appearance_monet_description">Available on Android 12+</string>
<string name="settings_appearance">Appearance</string>
<string name="settings_appearance_description">Theme, code style</string>
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
</resources>