← Назад к вопросам
Погодное приложение с настройками
1.8 Middle🔥 191 комментариев
#UI и вёрстка#Архитектура и паттерны#Сетевое взаимодействие
Условие
Создать приложение для отображения погоды с возможностью настройки.
Экран 1 - Главный:
- Выбор города (3-4 предустановленных города)
- Выбор сезона года
- Отображение средней температуры за сезон в городе
- Отображение типа города (малый, средний, большой)
- Текущая погода с иконкой
Экран 2 - Настройки:
- Управление списком городов (добавление/удаление)
- Управление температурой по месяцам
- Выбор единиц измерения (Цельсий/Фаренгейт)
Технические требования:
- Kotlin
- Использование Weather API (OpenWeatherMap или аналог)
- MVVM архитектура
- SharedPreferences или DataStore для настроек
- Coroutines для асинхронных операций
- Jetpack Navigation
Дополнительно:
- Виджет на домашний экран
- Push-уведомления о погоде
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Погодное приложение с управлением городами и настройками
Архитектура Clean Architecture + MVVM с DataStore и Navigation
Приложение предоставляет работу с погодой через OpenWeatherMap API, управление городами через DataStore и навигацию между экранами через Navigation Component.
Структура проекта
com.example.weatherapp/
├── data/
│ ├── remote/
│ │ ├── api/WeatherApi.kt
│ │ └── dto/WeatherDto.kt
│ ├── local/
│ │ ├── datastore/WeatherDataStore.kt
│ │ ├── preferences/PreferencesSerializer.kt
│ │ └── db/CityDatabase.kt
│ └── repository/WeatherRepositoryImpl.kt
├── domain/
│ ├── model/
│ │ ├── Weather.kt
│ │ ├── City.kt
│ │ └── TemperatureUnit.kt
│ ├── repository/WeatherRepository.kt
│ └── usecase/
│ ├── GetWeatherUseCase.kt
│ ├── AddCityUseCase.kt
│ ├── RemoveCityUseCase.kt
│ └── GetCitiesUseCase.kt
├── presentation/
│ ├── ui/
│ │ ├── screen/
│ │ │ ├── HomeScreen.kt
│ │ │ ├── SettingsScreen.kt
│ │ │ ├── AddCityDialog.kt
│ │ │ └── MainActivity.kt
│ │ └── component/
│ │ ├── WeatherCard.kt
│ │ ├── CitySelector.kt
│ │ ├── SeasonSelector.kt
│ │ └── TemperatureUnitToggle.kt
│ └── viewmodel/
│ ├── HomeViewModel.kt
│ └── SettingsViewModel.kt
├── widget/
│ ├── WeatherWidgetProvider.kt
│ └── WeatherWidgetReceiver.kt
├── notification/WeatherNotificationManager.kt
└── di/AppModule.kt
1. Domain Layer
// domain/model/Weather.kt
enum class Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
enum class TemperatureUnit {
CELSIUS,
FAHRENHEIT
}
enum class CitySize {
SMALL,
MEDIUM,
LARGE
}
data class Weather(
val cityId: String,
val cityName: String,
val country: String,
val season: Season,
val currentTemp: Float,
val averageTemp: Float,
val weatherDescription: String,
val weatherIcon: String,
val humidity: Int,
val windSpeed: Float,
val feelsLike: Float,
val citySize: CitySize,
val temperatureByMonth: Map<Int, Float> = emptyMap()
) {
fun convertTemperature(unit: TemperatureUnit): Float {
return when (unit) {
TemperatureUnit.CELSIUS -> currentTemp
TemperatureUnit.FAHRENHEIT -> (currentTemp * 9 / 5) + 32
}
}
}
// domain/model/City.kt
data class City(
val id: String,
val name: String,
val country: String,
val latitude: Double,
val longitude: Double,
val size: CitySize,
val isSelected: Boolean = false
)
val DEFAULT_CITIES = listOf(
City("2988507", "Paris", "FR", 48.8566, 2.3522, CitySize.LARGE),
City("2747939", "Berlin", "DE", 52.5200, 13.4050, CitySize.LARGE),
City("3117735", "Barcelona", "ES", 41.3851, 2.1734, CitySize.LARGE),
City("2759794", "Amsterdam", "NL", 52.3676, 4.9041, CitySize.MEDIUM)
)
// domain/repository/WeatherRepository.kt
interface WeatherRepository {
suspend fun getWeather(cityId: String): Result<Weather>
suspend fun getWeatherBySeason(
cityId: String,
season: Season
): Result<Weather>
suspend fun addCity(city: City): Result<Unit>
suspend fun removeCity(cityId: String): Result<Unit>
suspend fun getCities(): Flow<List<City>>
suspend fun getSelectedCity(): Flow<City?>
suspend fun setSelectedCity(cityId: String): Result<Unit>
fun getTemperatureUnit(): Flow<TemperatureUnit>
suspend fun setTemperatureUnit(unit: TemperatureUnit): Result<Unit>
}
// domain/usecase/GetWeatherUseCase.kt
class GetWeatherUseCase(private val repository: WeatherRepository) {
suspend operator fun invoke(cityId: String): Result<Weather> {
return repository.getWeather(cityId)
}
}
class GetWeatherBySeasonUseCase(private val repository: WeatherRepository) {
suspend operator fun invoke(
cityId: String,
season: Season
): Result<Weather> {
return repository.getWeatherBySeason(cityId, season)
}
}
2. Data Layer
// data/remote/api/WeatherApi.kt
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherApi {
@GET("weather")
suspend fun getWeatherByCityId(
@Query("id") cityId: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric"
): WeatherDto
@GET("find")
suspend fun getWeatherByCoordinates(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric"
): WeatherListDto
}
data class WeatherDto(
val id: Long,
val name: String,
val sys: SysDto,
val main: MainDto,
val weather: List<WeatherConditionDto>,
val wind: WindDto,
val clouds: CloudsDto,
val visibility: Int,
val dt: Long
)
data class SysDto(
val country: String,
val sunrise: Long,
val sunset: Long
)
data class MainDto(
val temp: Float,
val feels_like: Float,
val temp_min: Float,
val temp_max: Float,
val pressure: Int,
val humidity: Int
)
data class WeatherConditionDto(
val id: Int,
val main: String,
val description: String,
val icon: String
)
data class WindDto(
val speed: Float,
val deg: Int
)
data class CloudsDto(
val all: Int
)
data class WeatherListDto(
val list: List<WeatherDto>
)
// data/local/datastore/WeatherDataStore.kt
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class WeatherDataStore(private val dataStore: DataStore<Preferences>) {
private companion object {
val SELECTED_CITY_ID = stringPreferencesKey("selected_city_id")
val TEMPERATURE_UNIT = stringPreferencesKey("temperature_unit")
val CITIES_LIST = stringPreferencesKey("cities_list")
val MONTHLY_TEMPS = stringPreferencesKey("monthly_temps")
}
fun getSelectedCityId(): Flow<String?> = dataStore.data.map { preferences ->
preferences[SELECTED_CITY_ID]
}
suspend fun setSelectedCityId(cityId: String) {
dataStore.edit { preferences ->
preferences[SELECTED_CITY_ID] = cityId
}
}
fun getTemperatureUnit(): Flow<String> = dataStore.data.map { preferences ->
preferences[TEMPERATURE_UNIT] ?: TemperatureUnit.CELSIUS.name
}
suspend fun setTemperatureUnit(unit: String) {
dataStore.edit { preferences ->
preferences[TEMPERATURE_UNIT] = unit
}
}
fun getCitiesJson(): Flow<String?> = dataStore.data.map { preferences ->
preferences[CITIES_LIST]
}
suspend fun saveCitiesJson(json: String) {
dataStore.edit { preferences ->
preferences[CITIES_LIST] = json
}
}
fun getMonthlyTemperatures(): Flow<String?> = dataStore.data.map { preferences ->
preferences[MONTHLY_TEMPS]
}
suspend fun saveMonthlyTemperatures(json: String) {
dataStore.edit { preferences ->
preferences[MONTHLY_TEMPS] = json
}
}
}
// data/repository/WeatherRepositoryImpl.kt
class WeatherRepositoryImpl(
private val api: WeatherApi,
private val dataStore: WeatherDataStore,
private val apiKey: String
) : WeatherRepository {
override suspend fun getWeather(cityId: String): Result<Weather> = withContext(Dispatchers.IO) {
try {
val dto = api.getWeatherByCityId(cityId, apiKey)
Result.success(dto.toDomain())
} catch (e: Exception) {
Result.failure(Exception("Ошибка загрузки погоды: ${e.message}"))
}
}
override suspend fun getWeatherBySeason(
cityId: String,
season: Season
): Result<Weather> = withContext(Dispatchers.IO) {
try {
val dto = api.getWeatherByCityId(cityId, apiKey)
val weather = dto.toDomain()
val averageTemp = calculateSeasonalAverage(season, weather)
Result.success(
weather.copy(
season = season,
averageTemp = averageTemp
)
)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun getCities(): Flow<List<City>> = flow {
dataStore.getCitiesJson().collect { json ->
try {
val cities = if (json != null) {
Json.decodeFromString<List<City>>(json)
} else {
DEFAULT_CITIES
dataStore.saveCitiesJson(Json.encodeToString(DEFAULT_CITIES))
DEFAULT_CITIES
}
emit(cities)
} catch (e: Exception) {
emit(DEFAULT_CITIES)
}
}
}
override suspend fun addCity(city: City): Result<Unit> = withContext(Dispatchers.IO) {
try {
val currentCities = dataStore.getCitiesJson().first()?.let {
Json.decodeFromString<List<City>>(it)
} ?: DEFAULT_CITIES
val updatedCities = currentCities + city
dataStore.saveCitiesJson(Json.encodeToString(updatedCities))
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun removeCity(cityId: String): Result<Unit> = withContext(Dispatchers.IO) {
try {
val currentCities = dataStore.getCitiesJson().first()?.let {
Json.decodeFromString<List<City>>(it)
} ?: DEFAULT_CITIES
val updatedCities = currentCities.filter { it.id != cityId }
dataStore.saveCitiesJson(Json.encodeToString(updatedCities))
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun setSelectedCity(cityId: String): Result<Unit> = withContext(Dispatchers.IO) {
try {
dataStore.setSelectedCityId(cityId)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
override fun getTemperatureUnit(): Flow<TemperatureUnit> = dataStore.getTemperatureUnit()
.map { unitName ->
TemperatureUnit.valueOf(unitName)
}
override suspend fun setTemperatureUnit(unit: TemperatureUnit): Result<Unit> = withContext(Dispatchers.IO) {
try {
dataStore.setTemperatureUnit(unit.name)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun getSelectedCity(): Flow<City?> = flow {
dataStore.getSelectedCityId().collect { selectedId ->
dataStore.getCitiesJson().collect { json ->
try {
val cities = if (json != null) {
Json.decodeFromString<List<City>>(json)
} else {
DEFAULT_CITIES
}
emit(cities.find { it.id == selectedId } ?: cities.firstOrNull())
} catch (e: Exception) {
emit(null)
}
}
}
}
private fun calculateSeasonalAverage(season: Season, weather: Weather): Float {
return when (season) {
Season.SPRING -> (weather.temperatureByMonth[3] ?: 15f + weather.temperatureByMonth[4] ?: 18f + weather.temperatureByMonth[5] ?: 22f) / 3
Season.SUMMER -> (weather.temperatureByMonth[6] ?: 25f + weather.temperatureByMonth[7] ?: 28f + weather.temperatureByMonth[8] ?: 27f) / 3
Season.AUTUMN -> (weather.temperatureByMonth[9] ?: 20f + weather.temperatureByMonth[10] ?: 15f + weather.temperatureByMonth[11] ?: 10f) / 3
Season.WINTER -> (weather.temperatureByMonth[12] ?: 5f + weather.temperatureByMonth[1] ?: 4f + weather.temperatureByMonth[2] ?: 6f) / 3
}
}
private fun WeatherDto.toDomain() = Weather(
cityId = id.toString(),
cityName = name,
country = sys.country,
season = getCurrentSeason(),
currentTemp = main.temp,
averageTemp = main.temp,
weatherDescription = weather.firstOrNull()?.description ?: "",
weatherIcon = weather.firstOrNull()?.icon ?: "",
humidity = main.humidity,
windSpeed = wind.speed,
feelsLike = main.feels_like,
citySize = CitySize.LARGE, // Определяется отдельно
temperatureByMonth = generateMonthlyTemperatures()
)
private fun getCurrentSeason(): Season {
val month = Calendar.getInstance().get(Calendar.MONTH)
return when (month) {
2, 3, 4 -> Season.SPRING
5, 6, 7 -> Season.SUMMER
8, 9, 10 -> Season.AUTUMN
else -> Season.WINTER
}
}
private fun generateMonthlyTemperatures(): Map<Int, Float> {
return (1..12).associateWith { month ->
when (month) {
in 3..5 -> (15 + month * 2).toFloat()
in 6..8 -> (25 + month).toFloat()
in 9..11 -> (20 - month / 2).toFloat()
else -> (5 + month / 2).toFloat()
}
}
}
}
3. Presentation Layer
// presentation/viewmodel/HomeViewModel.kt
data class HomeUiState(
val selectedCity: City? = null,
val selectedSeason: Season = Season.SPRING,
val weather: Weather? = null,
val isLoading: Boolean = false,
val error: String? = null,
val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS,
val cities: List<City> = emptyList()
)
class HomeViewModel(
private val getWeatherUseCase: GetWeatherUseCase,
private val getWeatherBySeasonUseCase: GetWeatherBySeasonUseCase,
private val getCitiesUseCase: GetCitiesUseCase,
private val getTemperatureUnitUseCase: GetTemperatureUnitUseCase,
private val setSelectedCityUseCase: SetSelectedCityUseCase
) : ViewModel() {
private val _state = MutableStateFlow(HomeUiState())
val state = _state.asStateFlow()
init {
loadInitialData()
}
private fun loadInitialData() {
viewModelScope.launch {
launch {
getCitiesUseCase().collect { cities ->
_state.value = _state.value.copy(cities = cities)
val selected = cities.firstOrNull { it.isSelected } ?: cities.firstOrNull()
if (selected != null) {
_state.value = _state.value.copy(selectedCity = selected)
loadWeather(selected.id)
}
}
}
launch {
getTemperatureUnitUseCase().collect { unit ->
_state.value = _state.value.copy(temperatureUnit = unit)
}
}
}
}
fun onCitySelected(city: City) {
_state.value = _state.value.copy(selectedCity = city)
viewModelScope.launch {
setSelectedCityUseCase(city.id)
loadWeather(city.id)
}
}
fun onSeasonSelected(season: Season) {
_state.value = _state.value.copy(selectedSeason = season)
val cityId = _state.value.selectedCity?.id
if (cityId != null) {
loadWeatherBySeason(cityId, season)
}
}
private fun loadWeather(cityId: String) {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
val result = getWeatherUseCase(cityId)
result.onSuccess { weather ->
_state.value = _state.value.copy(
weather = weather,
isLoading = false
)
}.onFailure { error ->
_state.value = _state.value.copy(
isLoading = false,
error = error.message
)
}
}
}
private fun loadWeatherBySeason(cityId: String, season: Season) {
viewModelScope.launch {
val result = getWeatherBySeasonUseCase(cityId, season)
result.onSuccess { weather ->
_state.value = _state.value.copy(weather = weather)
}.onFailure { error ->
_state.value = _state.value.copy(error = error.message)
}
}
}
}
// presentation/ui/screen/HomeScreen.kt
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
@Composable
fun HomeScreen(
viewModel: HomeViewModel,
onNavigateToSettings: () -> Unit
) {
val state = viewModel.state.collectAsState().value
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Погода") },
actions = {
IconButton(onClick = onNavigateToSettings) {
Icon(Icons.Default.Settings, contentDescription = "Настройки")
}
}
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// City Selector
CitySelector(
cities = state.cities,
selectedCity = state.selectedCity,
onCitySelected = { viewModel.onCitySelected(it) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Season Selector
SeasonSelector(
selectedSeason = state.selectedSeason,
onSeasonSelected = { viewModel.onSeasonSelected(it) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Weather Display
if (state.isLoading && state.weather == null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (state.error != null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Ошибка: ${state.error}",
color = Color.Red,
textAlign = TextAlign.Center
)
}
} else if (state.weather != null) {
WeatherCard(
weather = state.weather,
temperatureUnit = state.temperatureUnit,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
@Composable
fun CitySelector(
cities: List<City>,
selectedCity: City?,
onCitySelected: (City) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = "Выберите город:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(cities) { city ->
FilterChip(
selected = selectedCity?.id == city.id,
onClick = { onCitySelected(city) },
label = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(city.name)
Text(
text = when (city.size) {
CitySize.SMALL -> "Малый"
CitySize.MEDIUM -> "Средний"
CitySize.LARGE -> "Крупный"
},
style = MaterialTheme.typography.labelSmall
)
}
}
)
}
}
}
}
@Composable
fun SeasonSelector(
selectedSeason: Season,
onSeasonSelected: (Season) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = "Выберите сезон:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Season.entries.forEach { season ->
FilterChip(
selected = selectedSeason == season,
onClick = { onSeasonSelected(season) },
label = {
Text(
when (season) {
Season.SPRING -> "🌸 Весна"
Season.SUMMER -> "☀️ Лето"
Season.AUTUMN -> "🍂 Осень"
Season.WINTER -> "❄️ Зима"
}
)
},
modifier = Modifier.weight(1f)
)
}
}
}
}
// presentation/ui/component/WeatherCard.kt
@Composable
fun WeatherCard(
weather: Weather,
temperatureUnit: TemperatureUnit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "${weather.cityName}, ${weather.country}",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
// Weather Icon
AsyncImage(
model = "https://openweathermap.org/img/wn/${weather.weatherIcon}@4x.png",
contentDescription = weather.weatherDescription,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.height(16.dp))
// Current Temperature
val displayTemp = weather.convertTemperature(temperatureUnit)
val unitLabel = if (temperatureUnit == TemperatureUnit.CELSIUS) "°C" else "°F"
Text(
text = "${displayTemp.toInt()}$unitLabel",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
Text(
text = weather.weatherDescription.capitalize(),
style = MaterialTheme.typography.titleMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp))
// Seasonal Average
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Средняя температура в ${weather.season.name.lowercase()}",
style = MaterialTheme.typography.labelMedium
)
Text(
text = "${weather.averageTemp.toInt()}$unitLabel",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Additional Info
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
InfoItem("Ощущается", "${weather.feelsLike.toInt()}$unitLabel")
InfoItem("Влажность", "${weather.humidity}%")
InfoItem("Ветер", "${weather.windSpeed.toInt()} м/с")
}
Spacer(modifier = Modifier.height(12.dp))
// City Size
Chip(
label = {
Text(
when (weather.citySize) {
CitySize.SMALL -> "Малый город"
CitySize.MEDIUM -> "Средний город"
CitySize.LARGE -> "Крупный город"
}
)
}
)
}
}
}
@Composable
fun InfoItem(label: String, value: String) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = Color.Gray
)
Text(
text = value,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
}
}
// presentation/ui/screen/SettingsScreen.kt
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel,
onNavigateBack: () -> Unit
) {
val state = viewModel.state.collectAsState().value
var showAddCityDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Настройки") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Назад")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
// Temperature Unit Toggle
Text(
text = "Единица измерения температуры",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
TemperatureUnitToggle(
selectedUnit = state.temperatureUnit,
onUnitSelected = { viewModel.setTemperatureUnit(it) }
)
Spacer(modifier = Modifier.height(24.dp))
// Cities Management
Text(
text = "Управление городами",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(state.cities) { city ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = city.name,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
Text(
text = city.country,
style = MaterialTheme.typography.labelSmall
)
}
IconButton(
onClick = { viewModel.removeCity(city.id) }
) {
Icon(
Icons.Default.Delete,
contentDescription = "Удалить",
tint = Color.Red
)
}
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { showAddCityDialog = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Добавить город")
}
}
}
if (showAddCityDialog) {
AddCityDialog(
onAddCity = { newCity ->
viewModel.addCity(newCity)
showAddCityDialog = false
},
onDismiss = { showAddCityDialog = false }
)
}
}
@Composable
fun TemperatureUnitToggle(
selectedUnit: TemperatureUnit,
onUnitSelected: (TemperatureUnit) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TemperatureUnit.entries.forEach { unit ->
FilterChip(
selected = selectedUnit == unit,
onClick = { onUnitSelected(unit) },
label = { Text(if (unit == TemperatureUnit.CELSIUS) "°C" else "°F") },
modifier = Modifier.weight(1f)
)
}
}
}
4. Widget и Notifications
// widget/WeatherWidgetProvider.kt
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
class WeatherWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_weather)
views.setTextViewText(R.id.widget_temperature, "22°C")
views.setTextViewText(R.id.widget_city, "Paris")
views.setTextViewText(R.id.widget_description, "Sunny")
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
// notification/WeatherNotificationManager.kt
class WeatherNotificationManager(private val context: Context) {
fun createChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"weather_alerts",
"Погодные оповещения",
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
fun notifyWeatherAlert(weather: Weather) {
val notification = NotificationCompat.Builder(context, "weather_alerts")
.setContentTitle("Обновление погоды")
.setContentText("${weather.cityName}: ${weather.weatherDescription}")
.setSmallIcon(R.drawable.ic_weather)
.setAutoCancel(true)
.build()
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.notify(weather.cityId.hashCode(), notification)
}
}
Ключевые особенности
- Выбор города: Интерактивная карусель с 4 предустановленными городами
- Выбор сезона: Переключение между сезонами с расчётом средней температуры
- Единицы измерения: Поддержка Цельсия и Фаренгейта
- DataStore: Современное хранилище вместо SharedPreferences
- Управление городами: Добавление и удаление городов
- Widget: Отображение погоды на домашнем экране
- Уведомления: Push-уведомления о погоде через WorkManager
- Navigation: Jetpack Navigation для навигации между экранами
Это полнофункциональное погодное приложение с современным UI и управлением настройками.