diff --git a/app/src/main/java/ru/otus/composehomework/ui/task1/Task1Screen.kt b/app/src/main/java/ru/otus/composehomework/ui/task1/Task1Screen.kt
index baea737..f8d0062 100644
--- a/app/src/main/java/ru/otus/composehomework/ui/task1/Task1Screen.kt
+++ b/app/src/main/java/ru/otus/composehomework/ui/task1/Task1Screen.kt
@@ -1,6 +1,15 @@
package ru.otus.composehomework.ui.task1
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
/**
* Задание 1: Переписывание XML на Compose
@@ -22,5 +31,31 @@ import androidx.compose.runtime.Composable
*/
@Composable
fun Task1Screen() {
- // TODO: Реализуйте экран на Compose
+ var welcomeText by remember { mutableStateOf("Добро пожаловать!") }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(32.dp)
+ ) {
+ Text(
+ text = welcomeText,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ Image(
+ painter = painterResource(id = android.R.drawable.ic_dialog_info),
+ contentDescription = "Welcome",
+ modifier = Modifier.size(120.dp)
+ )
+
+ Button(
+ onClick = { welcomeText = "Кнопка нажата!" }
+ ) {
+ Text("Нажми меня")
+ }
+ }
}
diff --git a/app/src/main/java/ru/otus/composehomework/ui/task2/Task2Screen.kt b/app/src/main/java/ru/otus/composehomework/ui/task2/Task2Screen.kt
index 9b625d4..c95c5eb 100644
--- a/app/src/main/java/ru/otus/composehomework/ui/task2/Task2Screen.kt
+++ b/app/src/main/java/ru/otus/composehomework/ui/task2/Task2Screen.kt
@@ -1,6 +1,15 @@
package ru.otus.composehomework.ui.task2
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
/**
* Задание 2: Добавление состояния
@@ -20,5 +29,34 @@ import androidx.compose.runtime.Composable
*/
@Composable
fun Task2Screen() {
- // TODO: Реализуйте экран с состоянием счетчика
+ var count by remember { mutableStateOf(0) }
+ val welcomeText = "Кнопка нажата $count раз!"
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(32.dp)
+ ) {
+ Text(
+ text = welcomeText,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ Image(
+ painter = painterResource(id = android.R.drawable.ic_dialog_info),
+ contentDescription = "Welcome",
+ modifier = Modifier.size(120.dp)
+ )
+
+ Text(text = "Счетчик: $count")
+
+ Button(
+ onClick = { count++ }
+ ) {
+ Text("Нажми меня")
+ }
+ }
}
diff --git a/app/src/main/java/ru/otus/composehomework/ui/task3/Task3Screen.kt b/app/src/main/java/ru/otus/composehomework/ui/task3/Task3Screen.kt
index 8cffe89..1a63a09 100644
--- a/app/src/main/java/ru/otus/composehomework/ui/task3/Task3Screen.kt
+++ b/app/src/main/java/ru/otus/composehomework/ui/task3/Task3Screen.kt
@@ -1,19 +1,46 @@
package ru.otus.composehomework.ui.task3
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ru.otus.composehomework.R
/**
* Задание 3: Подключение ViewModel и комплексное состояние (MVI паттерн)
- *
+ *
* Развивайте Task2Screen, подключив ViewModel для управления состоянием.
- *
+ *
* MVI (Model-View-Intent) паттерн:
* - State - единое состояние UI (Task3State) - определено в Task3Contracts.kt
* - Intent - действия пользователя (Task3Intent) - определено в Task3Contracts.kt
* - ViewModel обрабатывает Intent и обновляет State
- *
+ *
* Все контракты MVI (State, Intent, UiState) находятся в файле Task3Contracts.kt
- *
+ *
* Требования:
* 1. Скопируйте код из Task2Screen.kt (или начните с Task1Screen, если нужно)
* 2. Подключите ViewModel через: val viewModel: Task3ViewModel = viewModel()
@@ -33,7 +60,7 @@ import androidx.compose.runtime.Composable
* - UiState.Success -> показать результат
* - UiState.Error -> показать сообщение об ошибке
* - UiState.ValidationError -> показать ошибки валидации для каждого поля
- *
+ *
* Подсказки:
* - Используйте TextField с параметрами value и onValueChange
* - Для ошибок валидации используйте isError и supportingText
@@ -44,5 +71,247 @@ import androidx.compose.runtime.Composable
*/
@Composable
fun Task3Screen() {
- // TODO: Реализуйте экран с ViewModel и формой
+ val viewModel: Task3ViewModel = viewModel()
+ val state by viewModel.state.collectAsState()
+
+ FeedbackFormLayout(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ header = {
+ FeedbackHeader()
+ },
+ fields = {
+ FeedbackFields(
+ state = state,
+ onNameChange = { viewModel.handleIntent(Task3Intent.NameChanged(it)) },
+ onEmailChange = { viewModel.handleIntent(Task3Intent.EmailChanged(it)) },
+ onMessageChange = { viewModel.handleIntent(Task3Intent.MessageChanged(it)) }
+ )
+ },
+ actions = {
+ FeedbackActions(
+ uiState = state.uiState,
+ onSubmit = { viewModel.handleIntent(Task3Intent.SubmitClicked) },
+ onClear = { viewModel.handleIntent(Task3Intent.ClearClicked) }
+ )
+ },
+ status = {
+ FeedbackStatus(
+ uiState = state.uiState,
+ onRetry = { viewModel.handleIntent(Task3Intent.RetryClicked) }
+ )
+ }
+ )
+}
+
+/**
+ * Fixed: layout slot формы, который раскладывает элементы экрана по зонам.
+ * Рекомендация: такой подход позволяет вынести детали в отдельные composable
+ * и избегать "простыней" в коде.
+ */
+@Composable
+private fun FeedbackFormLayout(
+ modifier: Modifier = Modifier,
+ header: @Composable () -> Unit,
+ fields: @Composable ColumnScope.() -> Unit,
+ actions: @Composable ColumnScope.() -> Unit,
+ status: @Composable ColumnScope.() -> Unit
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ header()
+ fields()
+ actions()
+ status()
+ }
+}
+
+/**
+ * Заголовок формы.
+ */
+@Composable
+private fun FeedbackHeader() {
+ Text(
+ text = "Форма обратной связи",
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+}
+
+/**
+ * Блок полей формы. Работа с вводом и ошибками валидации.
+ */
+@Composable
+private fun ColumnScope.FeedbackFields(
+ state: Task3State,
+ onNameChange: (String) -> Unit,
+ onEmailChange: (String) -> Unit,
+ onMessageChange: (String) -> Unit
+) {
+ TextField(
+ value = state.name,
+ onValueChange = onNameChange,
+ label = { Text("Имя") },
+ placeholder = { Text(stringResource(R.string.name_hint)) },
+ modifier = Modifier.fillMaxWidth(),
+ isError = state.validationErrors?.errors?.get(FieldType.NAME) != null,
+ supportingText = state.validationErrors?.errors?.get(FieldType.NAME)?.let {
+ { Text(it) }
+ },
+ enabled = state.uiState !is UiState.Loading
+ )
+
+ TextField(
+ value = state.email,
+ onValueChange = onEmailChange,
+ label = { Text("Email") },
+ placeholder = { Text(stringResource(R.string.email_hint)) },
+ modifier = Modifier.fillMaxWidth(),
+ isError = state.validationErrors?.errors?.get(FieldType.EMAIL) != null,
+ supportingText = state.validationErrors?.errors?.get(FieldType.EMAIL)?.let {
+ { Text(it) }
+ },
+ enabled = state.uiState !is UiState.Loading
+ )
+
+ TextField(
+ value = state.message,
+ onValueChange = onMessageChange,
+ label = { Text("Сообщение") },
+ placeholder = { Text(stringResource(R.string.message_hint)) },
+ modifier = Modifier.fillMaxWidth(),
+ minLines = 3,
+ isError = state.validationErrors?.errors?.get(FieldType.MESSAGE) != null,
+ supportingText = state.validationErrors?.errors?.get(FieldType.MESSAGE)?.let {
+ { Text(it) }
+ },
+ enabled = state.uiState !is UiState.Loading
+ )
+}
+
+/**
+ * Блок с действиями - Отправка, очистка.
+ */
+@Composable
+private fun ColumnScope.FeedbackActions(
+ uiState: UiState,
+ onSubmit: () -> Unit,
+ onClear: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ onClick = onSubmit,
+ enabled = uiState !is UiState.Loading,
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(stringResource(R.string.submit))
+ }
+
+ OutlinedButton(
+ onClick = onClear,
+ enabled = uiState !is UiState.Loading,
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(stringResource(R.string.clear))
+ }
+ }
+}
+
+/**
+ * Визуализация состояния отправки формы (загрузка, успех, ошибка).
+ * Вся работа с UiState сконцентрирована в одном месте.
+ */
+@Composable
+private fun ColumnScope.FeedbackStatus(
+ uiState: UiState,
+ onRetry: () -> Unit
+) {
+ when (uiState) {
+ is UiState.Loading -> {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+
+ is UiState.Success -> {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = "Успешно!",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = "Форма успешно отправлена!",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Text(
+ text = "ID: ${uiState.result.id}",
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+
+ is UiState.Error -> {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = "Ошибка",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.error
+ )
+ Text(
+ text = uiState.message,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Button(
+ onClick = onRetry,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.retry))
+ }
+ }
+ }
+ }
+
+ is UiState.ValidationError -> {
+ // Ошибки валидации уже подсвечены возле полей ввода
+ }
+
+ is UiState.Idle -> {
+ // Форма готова к заполнению
+ }
+ }
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 26e74fc..c9469be 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,4 +13,5 @@
Отправить
Повторить
Загрузка...
+ Очистить