From a51b0cfb77c2f3a7812006226d0e18d349a14063 Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Mon, 26 Jan 2026 00:19:46 +0300
Subject: [PATCH 01/28] =?UTF-8?q?ANDR-5:=20=D0=98=D0=BD=D0=B8=D1=86=D0=B8?=
=?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=84=D0=B8?=
=?UTF-8?q?=D1=87=D0=B8=20Interview=20Trainer=20=D0=B8=20=D0=B2=D0=B5?=
=?UTF-8?q?=D1=80=D1=81=D1=82=D0=BA=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE?=
=?UTF-8?q?=D0=B3=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20CreateQui?=
=?UTF-8?q?z=20(#94)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Создан модуль Interview_Trainer
* Моделирование состояний первого экрана интервью тренажера - CreateQuiz
* Добавление необходимых ресурсов для верстки первого экрана экрана CreateQuiz
* Добавление базовых классов фичи
* Базовая верстка первого экрана CreateQuizScreen.kt
* Добавление объекта интервью тренажера в пути фич для будущей навигации
* ANDR-5: VoSpecialization перенесен внутри Loaded
* ANDR-5: SkillButton.kt компонент изменен. Убрана полная заливка кнопки при активации/нажатии на нее
* ANDR-5: Добавлены дополнительные строковые ресурсы для возможных ошибок
* ANDR-5: Создание превью и правки по отступам
* ANDR-5: Экран загрузки CreateQuiz
* ANDR-5: Использование экрана загрузки в главном для @StaticPreview
* ANDR-5: Удалены неиспользуемые строки ошибок
* ANDR-5: Убран лишний класс параметров. В StaticPreview передаем сразу стейт
* ANDR-5: Убрана преписка Mock у @Composable
* ANDR-5: id стало Long по аналогии с другими классами
* ANDR-5: Command'ы и Result'ы убраны из папки intents. Сама папка удалена
* ANDR-5: Создан интерфейс юзкейса
* ANDR-5: Созданы Domain модели
* ANDR-5: ВьюМоделька экрана CreateQuiz. Сделана не до конца
* ANDR-5: Изменение кол-ва вопросов через данные у ивента
* ANDR-5: изменил путь файла
* ANDR-5: Изменение кода экрана. Настроено правильное прокидывание лямбд в UI
* ANDR-5: Сделано Динамическое превью
* ANDR-5: убраны default параметры
* ANDR-5: убраны лишние классы
* ANDR-5: Создан маппер
* ANDR-5: имплементирован маппер и отдельный флоу ввода пользователя для изменения стейта
* ANDR-5: сделано рабочее динамик превью
* ANDR-5: убран default диспатчер при обновлении ввода пользователя
* ANDR-5: убраны параметры по умолчанию в верстке
---
.idea/gradle.xml | 9 +-
.../ru/yeahub/navigation_api/FeatureRoute.kt | 4 +
.../yeahub/core_ui/component/SkillButton.kt | 14 +-
.../interview-trainer/api/build.gradle.kts | 50 ++
.../api/src/main/AndroidManifest.xml | 4 +
.../api/InterviewTrainerApi.kt | 21 +
.../interview-trainer/impl/build.gradle.kts | 75 +++
.../impl/src/main/AndroidManifest.xml | 4 +
.../impl/InterviewTrainerFeatureImpl.kt | 21 +
.../presentation/CreateQuizCommand.kt | 10 +
.../presentation/CreateQuizEvent.kt | 16 +
.../presentation/CreateQuizResult.kt | 10 +
.../presentation/CreateQuizScreenMapper.kt | 14 +
.../presentation/CreateQuizState.kt | 22 +
.../presentation/CreateQuizViewModel.kt | 122 +++++
.../impl/createQuiz/ui/CreateQuizScreen.kt | 438 ++++++++++++++++++
.../createQuiz/ui/CreateQuizScreenLoading.kt | 131 ++++++
.../impl/src/main/res/drawable/minus_icon.xml | 11 +
.../impl/src/main/res/drawable/plus_icon.xml | 11 +
.../impl/src/main/res/values/strings.xml | 13 +
settings.gradle.kts | 3 +
21 files changed, 992 insertions(+), 11 deletions(-)
create mode 100644 feature/interview-trainer/api/build.gradle.kts
create mode 100644 feature/interview-trainer/api/src/main/AndroidManifest.xml
create mode 100644 feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
create mode 100644 feature/interview-trainer/impl/build.gradle.kts
create mode 100644 feature/interview-trainer/impl/src/main/AndroidManifest.xml
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/values/strings.xml
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d6d139c0..5e39de75 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -36,18 +36,21 @@
+
+
+
-
-
-
+
+
+
diff --git a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
index 446c60e6..bccd38fb 100644
--- a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
+++ b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
@@ -62,4 +62,8 @@ object FeatureRoute {
object PublicCollectionsFeature {
const val FEATURE_NAME = "public_collections"
}
+
+ object InterviewTrainerFeature {
+ const val FEATURE_NAME = "interview_trainer"
+ }
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
index 8294368a..83cb838b 100644
--- a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
@@ -138,12 +138,9 @@ fun DefaultButton(
val onSurfaceClick: () -> Unit = {
if (fillButton) {
- newContainerColor = if (newContainerColor == defaultColor) {
- purple
- } else {
- defaultColor
- }
- newContentColor = if (newContentColor == black) {
+ showBorder = !showBorder
+
+ newContentColor = if (showBorder) {
defaultColor
} else {
black
@@ -163,6 +160,7 @@ fun DefaultButton(
}
val border = when {
showBorder && !fillButton && !buttonWithoutBackground -> activeBorder()
+ fillButton && enabled && activeButton -> activeBorder()
fillButton && enabled -> null
buttonWithoutBackground -> null
else -> defaultsBorder()
@@ -171,7 +169,7 @@ fun DefaultButton(
if (newContentColor == black && buttonWithoutBackground && activeButton) {
purple
} else if (newContentColor == black && fillButton && activeButton) {
- defaultColor
+ black
} else {
newContentColor
}
@@ -182,7 +180,7 @@ fun DefaultButton(
enabled = enabled,
shape = shape,
color = if (activeButton && fillButton) {
- purple
+ defaultColor
} else {
if (buttonWithoutBackground) Color.Transparent else newContainerColor
},
diff --git a/feature/interview-trainer/api/build.gradle.kts b/feature/interview-trainer/api/build.gradle.kts
new file mode 100644
index 00000000..3de6e572
--- /dev/null
+++ b/feature/interview-trainer/api/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "ru.yeahub.interview_trainer.api"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.8"
+ }
+}
+
+dependencies {
+ implementation(project(":core:navigation-api"))
+
+ implementation(libs.androidx.core.ktx)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.runtime.android)
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/api/src/main/AndroidManifest.xml b/feature/interview-trainer/api/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/interview-trainer/api/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
new file mode 100644
index 00000000..b98d29ed
--- /dev/null
+++ b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
@@ -0,0 +1,21 @@
+package ru.yeahub.interview_trainer.api
+
+import androidx.compose.runtime.Composable
+
+/**
+ * API интерфейс для экрана тренажера собеседований.
+ *
+ * Демонстрирует:
+ * - Передачу параметров между экранами
+ */
+interface InterviewTrainerApi {
+ /**
+ * Экран тренажера собеседований.
+ *
+ * @param onBackClick Действие при нажатии кнопки "Назад"
+ */
+ @Composable
+ fun InterviewTrainerScreen(
+ onBackClick: () -> Unit,
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts
new file mode 100644
index 00000000..80db0ed2
--- /dev/null
+++ b/feature/interview-trainer/impl/build.gradle.kts
@@ -0,0 +1,75 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "ru.yeahub.interview_trainer.impl"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.8"
+ }
+}
+
+dependencies {
+ //Все нужные модули
+ implementation(project(":core:navigation-api"))
+ implementation(project(":core:network-api"))
+ implementation(project(":feature:interview-trainer:api"))
+ implementation(project(":core:utils"))
+ implementation(project(":core:ui"))
+
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.runtime.android)
+
+ implementation(libs.compose.shimmer)
+
+ //KOIN
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.compose)
+
+ // Navigation Compose
+ implementation(libs.androidx.navigation.compose)
+
+ // Timber
+ implementation(libs.timber)
+ implementation(libs.androidx.ui.tooling.preview.android)
+
+ testImplementation(libs.junit.jupiter)
+ testImplementation(platform(libs.junit.bom))
+ testRuntimeOnly(libs.junit.platform.launcher)
+ implementation(libs.androidx.junit.ktx)
+ testImplementation(libs.mockk)
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/AndroidManifest.xml b/feature/interview-trainer/impl/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
new file mode 100644
index 00000000..1c9a29f4
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
@@ -0,0 +1,21 @@
+package ru.yeahub.interview_trainer.impl
+
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import ru.yeahub.navigation_api.FeatureApi
+import ru.yeahub.navigation_api.FeatureRoute
+import ru.yeahub.navigation_api.NavigationPathManager
+
+class InterviewTrainerFeatureImpl : FeatureApi {
+ override fun getFeatureName(): String = FeatureRoute.InterviewTrainerFeature.FEATURE_NAME
+
+ override fun registerGraph(
+ navGraphBuilder: NavGraphBuilder,
+ navController: NavHostController,
+ pathManager: NavigationPathManager,
+ modifier: Modifier,
+ ) {
+ TODO("Not yet implemented")
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt
new file mode 100644
index 00000000..22d52a61
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizCommand.kt
@@ -0,0 +1,10 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+sealed interface CreateQuizCommand {
+ data class NavigateToInterviewQuizScreen(
+ val specializationId: Long,
+ val questionCount: Int
+ ) : CreateQuizCommand
+
+ data object NavigateBack : CreateQuizCommand
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt
new file mode 100644
index 00000000..c8df9ed8
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizEvent.kt
@@ -0,0 +1,16 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+sealed interface CreateQuizEvent {
+ data class OnSpecializationClick(val specializationId: Long) : CreateQuizEvent
+
+ data class OnPlusQuestionClick(val questionsCount: Int) : CreateQuizEvent
+
+ data class OnMinusQuestionClick(val questionsCount: Int) : CreateQuizEvent
+
+ data class OnStartInterviewQuizClick(
+ val specializationId: Long,
+ val questionCount: Int
+ ) : CreateQuizEvent
+
+ data object OnBackClick : CreateQuizEvent
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt
new file mode 100644
index 00000000..ed6bede3
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizResult.kt
@@ -0,0 +1,10 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+sealed interface CreateQuizResult {
+ data class NavigateToInterviewQuizScreen(
+ val specializationId: Long,
+ val questionCount: Int
+ ) : CreateQuizResult
+
+ data object NavigateBack : CreateQuizResult
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
new file mode 100644
index 00000000..47831a56
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
@@ -0,0 +1,14 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+object CreateQuizScreenMapper {
+
+ fun getScreenState(
+ specializations: List,
+ selectedSpecializationId: Long,
+ questionsCount: Int,
+ ): CreateQuizState = CreateQuizState.Loaded(
+ specializations = specializations,
+ selectedSpecializationId = selectedSpecializationId,
+ questionsCount = questionsCount
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
new file mode 100644
index 00000000..07a2d3c6
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
@@ -0,0 +1,22 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+import androidx.compose.runtime.Immutable
+
+sealed interface CreateQuizState {
+ //Изначальный
+ data object Loading : CreateQuizState
+
+ data class Loaded(
+ val specializations: List,
+ val selectedSpecializationId: Long,
+ val questionsCount: Int,
+ ) : CreateQuizState {
+ @Immutable
+ data class VoSpecialization(
+ val id: Long,
+ val title: String,
+ )
+ }
+
+ data class Error(val throwable: Throwable) : CreateQuizState
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
new file mode 100644
index 00000000..e39eaa93
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -0,0 +1,122 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import ru.yeahub.core_utils.BaseViewModel
+import ru.yeahub.interview_trainer.impl.createQuiz.ui.specializations
+
+open class CreateQuizViewModel(
+ private val screenMapper: CreateQuizScreenMapper,
+) : BaseViewModel() {
+
+ private val userInputState = MutableStateFlow(
+ UserInput(
+ selectedSpecializationId = 11,
+ questionsCount = MIN_QUESTIONS_COUNT
+ )
+ )
+
+ val screenState = userInputState
+ .map { userInput ->
+ screenMapper.getScreenState(
+ specializations = specializations,
+ selectedSpecializationId = userInput.selectedSpecializationId,
+ questionsCount = userInput.questionsCount
+ )
+ }.stateIn(
+ scope = viewModelScopeSafe,
+ started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
+ initialValue = CreateQuizState.Loading
+ )
+
+ private val _commands = MutableSharedFlow()
+ val commands: SharedFlow = _commands
+
+ fun onEvent(event: CreateQuizEvent) {
+ when (event) {
+ CreateQuizEvent.OnBackClick -> onBackClick()
+
+ is CreateQuizEvent.OnPlusQuestionClick -> incrementQuestionsCount(
+ questionsCount = event.questionsCount
+ )
+
+ is CreateQuizEvent.OnMinusQuestionClick -> decrementQuestionsCount(
+ questionsCount = event.questionsCount
+ )
+
+ is CreateQuizEvent.OnSpecializationClick -> changeChosenSpecialization(
+ newSpecializationId = event.specializationId
+ )
+
+ is CreateQuizEvent.OnStartInterviewQuizClick -> onStartInterviewClick(
+ specializationId = event.specializationId,
+ questionCount = event.questionCount
+ )
+ }
+ }
+
+ private fun onBackClick() {
+ viewModelScopeSafe.launch(Dispatchers.IO) {
+ _commands.emit(CreateQuizCommand.NavigateBack)
+ }
+ }
+
+ private fun incrementQuestionsCount(questionsCount: Int) {
+ viewModelScopeSafe.launch {
+ userInputState.update { currentInputState ->
+ val incrementedCount = questionsCount + 1
+ val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT)
+
+ currentInputState.copy(questionsCount = newCount)
+ }
+ }
+ }
+
+ private fun decrementQuestionsCount(questionsCount: Int) {
+ viewModelScopeSafe.launch {
+ userInputState.update { currentInputState ->
+ val incrementedCount = questionsCount - 1
+ val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT)
+
+ currentInputState.copy(questionsCount = newCount)
+ }
+ }
+ }
+
+ private fun changeChosenSpecialization(newSpecializationId: Long) {
+ viewModelScopeSafe.launch {
+ userInputState.update { currentInputState ->
+ currentInputState.copy(selectedSpecializationId = newSpecializationId)
+ }
+ }
+ }
+
+ private fun onStartInterviewClick(specializationId: Long, questionCount: Int) {
+ viewModelScopeSafe.launch(Dispatchers.IO) {
+ _commands.emit(
+ CreateQuizCommand.NavigateToInterviewQuizScreen(
+ specializationId = specializationId,
+ questionCount = questionCount
+ )
+ )
+ }
+ }
+
+ private data class UserInput(
+ val selectedSpecializationId: Long,
+ val questionsCount: Int,
+ )
+
+ companion object {
+ private const val MIN_QUESTIONS_COUNT = 1
+ private const val MAX_QUESTIONS_COUNT = 8
+ private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L
+ }
+}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
new file mode 100644
index 00000000..917a3cc5
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -0,0 +1,438 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.ui
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.delay
+import ru.yeahub.core_ui.component.ErrorScreen
+import ru.yeahub.core_ui.component.PrimaryButton
+import ru.yeahub.core_ui.component.SkillButton
+import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
+import ru.yeahub.core_ui.example.dynamicPreview.ProvidePreviewCompositionLocals
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.LocalAppTypography
+import ru.yeahub.core_ui.theme.colors
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreenMapper
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
+
+private val FIGMA_HORIZONTAL_PADDING = 16.dp
+private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
+private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
+
+@Composable
+private fun ScreenUI(
+ state: CreateQuizState,
+ onEvent: (CreateQuizEvent) -> Unit,
+ headerText: TextOrResource,
+) {
+ Scaffold(
+ containerColor = colors.black10,
+ topBar = {
+ TopAppBarWithBottomBorder(
+ title = headerText,
+ onBackClick = { onEvent(CreateQuizEvent.OnBackClick) }
+ )
+ }
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ when (state) {
+ CreateQuizState.Loading -> CreateQuizLoading()
+
+ is CreateQuizState.Error -> {
+ ErrorScreen(
+ error = state.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = { onEvent(CreateQuizEvent.OnBackClick) }
+ )
+ }
+
+ is CreateQuizState.Loaded -> BaseCreateQuizScreen(
+ specializations = state.specializations,
+ selectedSpecializationId = state.selectedSpecializationId,
+ questionsCount = state.questionsCount,
+ onSpecializationClick = { id ->
+ onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
+ },
+ onPlusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
+ },
+ onMinusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
+ },
+ onStartQuizClick = { specializationId: Long, questionsCount: Int ->
+ onEvent(
+ CreateQuizEvent.OnStartInterviewQuizClick(
+ specializationId = specializationId,
+ questionCount = questionsCount,
+ )
+ )
+ },
+ titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun BaseCreateQuizScreen(
+ specializations: List,
+ selectedSpecializationId: Long,
+ questionsCount: Int,
+ onSpecializationClick: (id: Long) -> Unit,
+ onPlusQuestionCountClick: (count: Int) -> Unit,
+ onMinusQuestionCountClick: (count: Int) -> Unit,
+ onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
+ titleText: TextOrResource,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+
+ Column(
+ modifier = modifier
+ .padding(horizontal = FIGMA_HORIZONTAL_PADDING),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
+ text = titleText.getString(context),
+ style = LocalAppTypography.current.head5,
+ )
+
+ ChooseSpecializationBlock(
+ context = context,
+ specializations = specializations,
+ selectedSpecializationId = selectedSpecializationId,
+ onSpecializationClick = onSpecializationClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
+ )
+
+ Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+
+ ChooseQuestionsCountBlock(
+ context = context,
+ questionsCount = questionsCount,
+ onPlusQuestionCountClick = onPlusQuestionCountClick,
+ onMinusQuestionCountClick = onMinusQuestionCountClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ StartQuizButton(
+ specializationId = selectedSpecializationId,
+ questionsCount = questionsCount,
+ onStartQuizClick = onStartQuizClick,
+ )
+ }
+}
+
+@Composable
+private fun ChooseSpecializationBlock(
+ specializations: List,
+ selectedSpecializationId: Long,
+ context: Context,
+ onSpecializationClick: (Long) -> Unit,
+ titleText: TextOrResource,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier) {
+ Text(
+ style = LocalAppTypography.current.body3Accent,
+ text = titleText.getString(context),
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(
+ space = 12.dp,
+ alignment = Alignment.Start
+ ),
+ verticalArrangement = Arrangement.spacedBy(
+ space = 12.dp,
+ alignment = Alignment.Top
+ ),
+ ) {
+ specializations.forEach { specialization ->
+ SkillButton(
+ contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
+ enabled = true,
+ activeButton = specialization.id == selectedSpecializationId,
+ fillButton = true,
+ text = specialization.title,
+ onClick = { onSpecializationClick(specialization.id) },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ChooseQuestionsCountBlock(
+ context: Context,
+ questionsCount: Int,
+ onPlusQuestionCountClick: (count: Int) -> Unit,
+ onMinusQuestionCountClick: (count: Int) -> Unit,
+ titleText: TextOrResource,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier) {
+ Text(
+ style = LocalAppTypography.current.body3Accent,
+ text = titleText.getString(context),
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ QuestionCounter(
+ count = questionsCount,
+ onPlusQuestionCountClick = onPlusQuestionCountClick,
+ onMinusQuestionCountClick = onMinusQuestionCountClick
+ )
+ }
+}
+
+@Composable
+private fun QuestionCounter(
+ onPlusQuestionCountClick: (count: Int) -> Unit,
+ onMinusQuestionCountClick: (count: Int) -> Unit,
+ modifier: Modifier = Modifier,
+ count: Int,
+) {
+ Surface(
+ modifier = modifier,
+ shape = RoundedCornerShape(12.dp),
+ color = colors.black50,
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(vertical = 6.dp, horizontal = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ modifier = Modifier.size(24.dp),
+ onClick = { onMinusQuestionCountClick(count) }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.minus_icon),
+ contentDescription = "Decrease questions count",
+ tint = colors.black600
+ )
+ }
+
+ Text(
+ text = count.toString(),
+ style = LocalAppTypography.current.body5Accent,
+ textAlign = TextAlign.Center,
+ color = colors.black600
+ )
+
+ IconButton(
+ modifier = Modifier.size(24.dp),
+ onClick = { onPlusQuestionCountClick(count) }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.plus_icon),
+ contentDescription = "Increase questions count",
+ tint = colors.black600,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun StartQuizButton(
+ specializationId: Long,
+ questionsCount: Int,
+ onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PrimaryButton(
+ modifier = modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
+ .height(48.dp)
+ .fillMaxWidth(),
+ onClick = { onStartQuizClick(specializationId, questionsCount) }
+ ) {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Начать",
+ style = LocalAppTypography.current.body3Strong,
+ textAlign = TextAlign.Center,
+ color = colors.white900
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(ru.yeahub.ui.R.drawable.arrow_next),
+ contentDescription = "Start Interview Quiz Session",
+ tint = colors.white900
+ )
+ }
+ }
+}
+
+val specializations = listOf(
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 11,
+ title = "Frontend"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 1,
+ title = "Backend"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 2,
+ title = "Data Science"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 3,
+ title = "Machine Learning"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 4,
+ title = "Testing"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 5,
+ title = "iOS Dev"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 21,
+ title = "Android Dev"
+ ),
+ CreateQuizState.Loaded.VoSpecialization(
+ id = 6,
+ title = "Game Dev"
+ )
+)
+
+class CreateQuizScreenStateParamProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(
+ CreateQuizState.Loaded(
+ specializations = specializations,
+ selectedSpecializationId = 11,
+ questionsCount = 1
+ ),
+ CreateQuizState.Loading,
+ CreateQuizState.Error(
+ Throwable("Не удалось загрузить данные")
+ )
+ )
+}
+
+@StaticPreview
+@Composable
+fun CreateQuizScreenPreview(
+ @PreviewParameter(CreateQuizScreenStateParamProvider::class)
+ state: CreateQuizState,
+) {
+ ScreenUI(
+ state = state,
+ onEvent = { },
+ headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DynamicPreviewUI() {
+ val mockViewModel = viewModelCreator {
+ CreateQuizViewModel(CreateQuizScreenMapper)
+ }
+
+ val state by mockViewModel.screenState.collectAsState()
+
+ LaunchedEffect(Unit) {
+ delay(RESPONSE_DELAY)
+ //Изначальное кол-во == 1
+ mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1))
+ delay(RESPONSE_DELAY)
+ // должно быть 2
+ mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2))
+ delay(RESPONSE_DELAY)
+ // должно быть 3
+ mockViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3))
+ delay(RESPONSE_DELAY)
+ // должно быть снова 2
+ mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(21))
+ // С изначально выбранного Frontend Dev должно быть выбрано Android Dev
+ }
+
+ ProvidePreviewCompositionLocals {
+ ScreenUI(
+ state = state,
+ onEvent = mockViewModel::onEvent,
+ headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ )
+ }
+}
+
+typealias ViewModelCreator = () -> ViewModel?
+
+class ViewModelFactory(
+ private val viewModelCreator: ViewModelCreator = { null },
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = viewModelCreator() as T
+}
+
+@Composable
+inline fun viewModelCreator(noinline creator: ViewModelCreator): VM =
+ viewModel(factory = remember { ViewModelFactory(creator) })
+
+private const val RESPONSE_DELAY = 1500L
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
new file mode 100644
index 00000000..29138a7f
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
@@ -0,0 +1,131 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.valentinilk.shimmer.shimmer
+import ru.yeahub.core_ui.component.SecondaryButton
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.LocalAppTypography
+import ru.yeahub.core_ui.theme.colors
+import ru.yeahub.interview_trainer.impl.R
+
+private val FIGMA_HORIZONTAL_PADDING = 16.dp
+private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
+private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
+
+@StaticPreview
+@Composable
+fun CreateQuizLoading(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .padding(horizontal = FIGMA_HORIZONTAL_PADDING)
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(vertical = 24.dp),
+ style = LocalAppTypography.current.head5,
+ text = stringResource(R.string.create_quiz_screen_main_title),
+ color = colors.black900
+ )
+
+ PlaceHolderBlock()
+
+ Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+
+ PlaceHolderBlock()
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ DisabledStartQuizButton()
+ }
+}
+
+@Composable
+private fun PlaceHolderBlock(
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .shimmer(),
+ shape = RoundedCornerShape(8.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(32.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(4.dp))
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(148.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(4.dp))
+ )
+ }
+}
+
+@Composable
+private fun DisabledStartQuizButton(
+ modifier: Modifier = Modifier,
+) {
+ SecondaryButton(
+ modifier = modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
+ .height(48.dp)
+ .fillMaxWidth(),
+ enabled = false,
+ onClick = { }
+ ) {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.create_quiz_start_quiz_button_text),
+ style = LocalAppTypography.current.body3Strong,
+ textAlign = TextAlign.Center,
+ color = colors.white900
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(ru.yeahub.ui.R.drawable.arrow_next),
+ contentDescription = "Start Interview Quiz Session",
+ tint = colors.white900
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml
new file mode 100644
index 00000000..cb791905
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable/minus_icon.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml
new file mode 100644
index 00000000..61d61034
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable/plus_icon.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
new file mode 100644
index 00000000..c72f7ccf
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+
+
+ Подготовка
+ Собеседование
+ Выбор специализации
+ Количество вопросов
+ Начать
+
+ УПС!
+ Что‑то пошло не так
+ Назад
+ Не удалось загрузить данные
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 616cc767..facd22a9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -58,3 +58,6 @@ include(":feature:questions-or-collections")
include(":feature:questions-or-collections:api")
include(":feature:questions-or-collections:impl")
+include(":feature:interview-trainer")
+include(":feature:interview-trainer:api")
+include(":feature:interview-trainer:impl")
From 091ef9e39baf1ab86d158ef86b36394a03fdcb23 Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Fri, 30 Jan 2026 23:22:32 +0300
Subject: [PATCH 02/28] =?UTF-8?q?ANDR-5:=20CreateQuiz=20=D0=B2=D1=82=D0=BE?=
=?UTF-8?q?=D1=80=D0=BE=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF.=20Data=20+=20Dom?=
=?UTF-8?q?ain=20=20=20(#95)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-5: Создан маппер
* ANDR-5: имплементирован маппер и отдельный флоу ввода пользователя для изменения стейта
* ANDR-5: сделано рабочее динамик превью
* ANDR-5: убран default диспатчер при обновлении ввода пользователя
* ANDR-5: убраны параметры по умолчанию в верстке
* ANDR-5: Созданы нужные бизнес модели
* ANDR-5: Создан интерфейс репозитория
* ANDR-5: ЮзКейс + добавлены параметры страницы и общего кол-ва в запрос
* ANDR-5: реквест вынес в отдельный файл
* ANDR-5: подключен модуль с тестами
* ANDR-5: Создан маппер дата в домейн
* ANDR-5: Сделана реализация репозитория
* ANDR-5: Пройдены успешные тесты на DataToDomainMapper
* ANDR-5: Юзкейс убран (на данном этапе не нужен)
* ANDR-5: Правки.Вторая функция в маппере приватная; Классы для тестов перенесены внутрь класса самих тестов; Сам тест сокращен до 1 функции и 1 аргументПровайдера
* ANDR-5: Правки. Домейн классы имеют только гарантированные поля
* ANDR-5: убраны лишние параметры из тестов
* ANDR-5: переименовал функцию на map
---
.../interview-trainer/impl/build.gradle.kts | 1 +
.../data/CreateQuizDataToDomainMapper.kt | 22 +++++
.../data/CreateQuizRepositoryImpl.kt | 20 +++++
.../domain/CreateQuizRepositoryApi.kt | 7 ++
.../createQuiz/domain/DomainSpecialization.kt | 6 ++
.../DomainSpecializationListResponse.kt | 6 ++
.../domain/SpecializationsRequest.kt | 6 ++
.../test/java/test/CreateQuizMapperTest.kt | 82 +++++++++++++++++++
8 files changed, 150 insertions(+)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizDataToDomainMapper.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizRepositoryImpl.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/CreateQuizRepositoryApi.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/SpecializationsRequest.kt
create mode 100644 feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt
diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts
index 80db0ed2..1d5e92b4 100644
--- a/feature/interview-trainer/impl/build.gradle.kts
+++ b/feature/interview-trainer/impl/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation(project(":core:navigation-api"))
implementation(project(":core:network-api"))
implementation(project(":feature:interview-trainer:api"))
+ implementation(project(":core:test"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizDataToDomainMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizDataToDomainMapper.kt
new file mode 100644
index 00000000..1fe199a6
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizDataToDomainMapper.kt
@@ -0,0 +1,22 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.data
+
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse
+import ru.yeahub.network_api.models.GetSpecializationResponse
+import ru.yeahub.network_api.models.GetSpecializationsResponse
+
+class CreateQuizDataToDomainMapper {
+ fun mapDataListToDomainList(
+ dataResponse: GetSpecializationsResponse,
+ ): DomainSpecializationListResponse =
+ DomainSpecializationListResponse(
+ data = dataResponse.data.map { dataItem -> dataToDomain(dataItem) },
+ total = dataResponse.total
+ )
+
+ private fun dataToDomain(data: GetSpecializationResponse): DomainSpecialization =
+ DomainSpecialization(
+ id = data.id,
+ title = data.title
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizRepositoryImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizRepositoryImpl.kt
new file mode 100644
index 00000000..8cdfaab4
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/data/CreateQuizRepositoryImpl.kt
@@ -0,0 +1,20 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.data
+
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.CreateQuizRepositoryApi
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.SpecializationsRequest
+import ru.yeahub.network_api.NetworkProvider
+
+class CreateQuizRepositoryImpl(
+ private val apiService: NetworkProvider,
+ private val mapper: CreateQuizDataToDomainMapper,
+) : CreateQuizRepositoryApi {
+ override suspend fun getSpecializationsList(
+ request: SpecializationsRequest,
+ ): DomainSpecializationListResponse = mapper.mapDataListToDomainList(
+ dataResponse = apiService.apiService.getSpecializations(
+ page = request.page,
+ limit = request.limit
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/CreateQuizRepositoryApi.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/CreateQuizRepositoryApi.kt
new file mode 100644
index 00000000..1d58f4eb
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/CreateQuizRepositoryApi.kt
@@ -0,0 +1,7 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+interface CreateQuizRepositoryApi {
+ suspend fun getSpecializationsList(
+ request: SpecializationsRequest,
+ ): DomainSpecializationListResponse
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt
new file mode 100644
index 00000000..00cd8348
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecialization.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+data class DomainSpecialization(
+ val id: Long,
+ val title: String,
+)
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt
new file mode 100644
index 00000000..2d032a21
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/DomainSpecializationListResponse.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+data class DomainSpecializationListResponse(
+ val total: Long,
+ val data: List,
+)
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/SpecializationsRequest.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/SpecializationsRequest.kt
new file mode 100644
index 00000000..3b5eb858
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/SpecializationsRequest.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+data class SpecializationsRequest(
+ val page: Int,
+ val limit: Int,
+)
diff --git a/feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt
new file mode 100644
index 00000000..59282f55
--- /dev/null
+++ b/feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt
@@ -0,0 +1,82 @@
+package test
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.interview_trainer.impl.createQuiz.data.CreateQuizDataToDomainMapper
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse
+import ru.yeahub.network_api.models.GetSpecializationResponse
+import ru.yeahub.network_api.models.GetSpecializationsResponse
+import ru.yeahub.test.TestArgumentsProvider
+
+class CreateQuizMapperTest {
+ private val toDomainMapper = CreateQuizDataToDomainMapper()
+
+ //CreateQuizDataToDomainMapper параметризированный тест
+ class ArgumentsProvider :
+ TestArgumentsProvider() {
+ override fun testCases(): List = listOf(
+ SpecializationSelectionDataToDomainMapperTestCase(
+ dataToTest = SpecializationExampleDataClasses.defaultSpecialListResponse,
+ expectedResult = SpecializationExampleDataClasses.defaultDomainSpecialListResponse
+ )
+ )
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(ArgumentsProvider::class)
+ fun specializationSelectionDataToDomainMapperTestCase(
+ testCase: SpecializationSelectionDataToDomainMapperTestCase,
+ ) {
+ val result = toDomainMapper.mapDataListToDomainList(testCase.dataToTest)
+ Assertions.assertEquals(testCase.expectedResult, result)
+ }
+
+ object SpecializationExampleDataClasses {
+ val defaultSpecialResponse = GetSpecializationResponse(
+ id = 0L,
+ title = "default title num 0",
+ description = "default description",
+ imageSrc = null,
+ createdAt = "01.01.1970",
+ updatedAt = "01.01.1970"
+ )
+
+ val defaultSpecialResponseWithImage = GetSpecializationResponse(
+ id = 1L,
+ title = "default title num 1",
+ description = "default description",
+ imageSrc = "some image url",
+ createdAt = "01.01.1970",
+ updatedAt = "01.01.1970"
+ )
+
+ val defaultDomainSpecial = DomainSpecialization(
+ id = 0L,
+ title = "default title num 0"
+ )
+
+ val defaultDomainSpecialWithImage = DomainSpecialization(
+ id = 1L,
+ title = "default title num 1"
+ )
+
+ val defaultSpecialListResponse = GetSpecializationsResponse(
+ page = 1L,
+ limit = 10L,
+ data = listOf(defaultSpecialResponse, defaultSpecialResponseWithImage),
+ total = 2L
+ )
+
+ val defaultDomainSpecialListResponse = DomainSpecializationListResponse(
+ data = listOf(defaultDomainSpecial, defaultDomainSpecialWithImage),
+ total = 2L
+ )
+ }
+
+ data class SpecializationSelectionDataToDomainMapperTestCase(
+ val dataToTest: GetSpecializationsResponse,
+ val expectedResult: DomainSpecializationListResponse,
+ )
+}
\ No newline at end of file
From f36f58949bd3d79dc378602c69db5001a66cb24d Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Mon, 2 Feb 2026 15:00:46 +0300
Subject: [PATCH 03/28] =?UTF-8?q?[ANDR-54]=20ANDR-70:=20=D0=A2=D1=80=D0=B5?=
=?UTF-8?q?=D1=82=D0=B8=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF=20CreateQuizScree?=
=?UTF-8?q?n.=20=D0=9B=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20+=20=D0=A2=D0=B5?=
=?UTF-8?q?=D1=81=D1=82=D1=8B=20(#100)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-5: добавлен юзкейс
* ANDR-5: Скрин маппер теперь маппит домейн в VO
* ANDR-5: Юзкейс разделен на интерфейс и импл
* ANDR-5: Подключение юзкейса и маппера во вьюмодели
* ANDR-5: Удаление лишних созданий корутин при изменении ввода юзера (и стейта)
* ANDR-5: Переименован файл теста маппера dataToDomain
* ANDR-5: Новый тест файл для теста ScreenMapper'а
* ANDR-5: Правки для ktlint
* ANDR-5: Проставлен везде is для удобства
* ANDR-5: Удалены ненужные файлы тестов (уже удаленные)
* ANDR-5: Параметризированные тесты вынесены наверх в самих классах тестов
---
.../GetSpecializationsListUseCaseImpl.kt | 9 ++
.../domain/GetSpecializationsUseCase.kt | 5 +
.../presentation/CreateQuizScreenMapper.kt | 11 ++-
.../presentation/CreateQuizViewModel.kt | 34 +++----
.../impl/createQuiz/ui/CreateQuizScreen.kt | 50 +++++++++-
...kt => CreateQuizDataToDomainMapperTest.kt} | 23 +++--
.../java/test/CreateQuizScreenMapperTest.kt | 89 +++++++++++++++++
.../test/SpecializationExampleDataClasses.kt | 62 ------------
.../test/SpecializationSelectionMapperTest.kt | 99 -------------------
9 files changed, 185 insertions(+), 197 deletions(-)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt
rename feature/interview-trainer/impl/src/test/java/test/{CreateQuizMapperTest.kt => CreateQuizDataToDomainMapperTest.kt} (96%)
create mode 100644 feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
delete mode 100644 feature/selection-specializations/impl/src/test/java/test/SpecializationExampleDataClasses.kt
delete mode 100644 feature/selection-specializations/impl/src/test/java/test/SpecializationSelectionMapperTest.kt
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
new file mode 100644
index 00000000..ac3e52f8
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
@@ -0,0 +1,9 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+class GetSpecializationsListUseCaseImpl(
+ private val repository: CreateQuizRepositoryApi,
+) : GetSpecializationsUseCase {
+ override suspend fun invoke(
+ request: SpecializationsRequest,
+ ): DomainSpecializationListResponse = repository.getSpecializationsList(request = request)
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt
new file mode 100644
index 00000000..4171b335
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt
@@ -0,0 +1,5 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.domain
+
+interface GetSpecializationsUseCase {
+ suspend operator fun invoke(request: SpecializationsRequest): DomainSpecializationListResponse
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
index 47831a56..1abd7353 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
@@ -1,13 +1,20 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
+
object CreateQuizScreenMapper {
fun getScreenState(
- specializations: List,
+ specializations: List,
selectedSpecializationId: Long,
questionsCount: Int,
): CreateQuizState = CreateQuizState.Loaded(
- specializations = specializations,
+ specializations = specializations.map { domainSpec ->
+ CreateQuizState.Loaded.VoSpecialization(
+ id = domainSpec.id,
+ title = domainSpec.title
+ )
+ },
selectedSpecializationId = selectedSpecializationId,
questionsCount = questionsCount
)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
index e39eaa93..1a163104 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -10,9 +10,11 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.yeahub.core_utils.BaseViewModel
-import ru.yeahub.interview_trainer.impl.createQuiz.ui.specializations
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsUseCase
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.SpecializationsRequest
open class CreateQuizViewModel(
+ private val getSpecializationsListUseCase: GetSpecializationsUseCase,
private val screenMapper: CreateQuizScreenMapper,
) : BaseViewModel() {
@@ -25,8 +27,10 @@ open class CreateQuizViewModel(
val screenState = userInputState
.map { userInput ->
+ val request = SpecializationsRequest(page = 1, limit = 99)
+
screenMapper.getScreenState(
- specializations = specializations,
+ specializations = getSpecializationsListUseCase(request).data,
selectedSpecializationId = userInput.selectedSpecializationId,
questionsCount = userInput.questionsCount
)
@@ -69,32 +73,26 @@ open class CreateQuizViewModel(
}
private fun incrementQuestionsCount(questionsCount: Int) {
- viewModelScopeSafe.launch {
- userInputState.update { currentInputState ->
- val incrementedCount = questionsCount + 1
- val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT)
+ userInputState.update { currentInputState ->
+ val incrementedCount = questionsCount + 1
+ val newCount = incrementedCount.coerceAtMost(MAX_QUESTIONS_COUNT)
- currentInputState.copy(questionsCount = newCount)
- }
+ currentInputState.copy(questionsCount = newCount)
}
}
private fun decrementQuestionsCount(questionsCount: Int) {
- viewModelScopeSafe.launch {
- userInputState.update { currentInputState ->
- val incrementedCount = questionsCount - 1
- val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT)
+ userInputState.update { currentInputState ->
+ val incrementedCount = questionsCount - 1
+ val newCount = incrementedCount.coerceAtLeast(MIN_QUESTIONS_COUNT)
- currentInputState.copy(questionsCount = newCount)
- }
+ currentInputState.copy(questionsCount = newCount)
}
}
private fun changeChosenSpecialization(newSpecializationId: Long) {
- viewModelScopeSafe.launch {
- userInputState.update { currentInputState ->
- currentInputState.copy(selectedSpecializationId = newSpecializationId)
- }
+ userInputState.update { currentInputState ->
+ currentInputState.copy(selectedSpecializationId = newSpecializationId)
}
}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index 917a3cc5..8ef23837 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -38,6 +38,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import ru.yeahub.core_ui.component.ErrorScreen
import ru.yeahub.core_ui.component.PrimaryButton
import ru.yeahub.core_ui.component.SkillButton
@@ -47,8 +48,15 @@ import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.core_utils.common.observe
import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsUseCase
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.SpecializationsRequest
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizCommand
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizResult
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreenMapper
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
@@ -117,6 +125,24 @@ private fun ScreenUI(
}
}
+@Composable
+fun HandleCommand(
+ commandFlow: Flow,
+ onResult: (CreateQuizResult) -> Unit,
+) {
+ commandFlow.observe { command ->
+ when (command) {
+ is CreateQuizCommand.NavigateBack -> onResult(CreateQuizResult.NavigateBack)
+ is CreateQuizCommand.NavigateToInterviewQuizScreen -> onResult(
+ CreateQuizResult.NavigateToInterviewQuizScreen(
+ specializationId = command.specializationId,
+ questionCount = command.questionCount
+ )
+ )
+ }
+ }
+}
+
@Composable
private fun BaseCreateQuizScreen(
specializations: List,
@@ -390,11 +416,27 @@ fun CreateQuizScreenPreview(
@Preview(showBackground = true)
@Composable
fun DynamicPreviewUI() {
+ val mockDomainList = specializations.map { voSpec ->
+ DomainSpecialization(id = voSpec.id, title = voSpec.title)
+ }
+
+ val mockUseCase = object : GetSpecializationsUseCase {
+ override suspend fun invoke(
+ request: SpecializationsRequest,
+ ): DomainSpecializationListResponse {
+ delay(RESPONSE_DELAY)
+ return DomainSpecializationListResponse(
+ total = mockDomainList.size.toLong(),
+ data = mockDomainList
+ )
+ }
+ }
+
val mockViewModel = viewModelCreator {
- CreateQuizViewModel(CreateQuizScreenMapper)
+ CreateQuizViewModel(mockUseCase, CreateQuizScreenMapper)
}
- val state by mockViewModel.screenState.collectAsState()
+ val mockState by mockViewModel.screenState.collectAsState()
LaunchedEffect(Unit) {
delay(RESPONSE_DELAY)
@@ -408,13 +450,13 @@ fun DynamicPreviewUI() {
mockViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3))
delay(RESPONSE_DELAY)
// должно быть снова 2
- mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(21))
+ mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(27))
// С изначально выбранного Frontend Dev должно быть выбрано Android Dev
}
ProvidePreviewCompositionLocals {
ScreenUI(
- state = state,
+ state = mockState,
onEvent = mockViewModel::onEvent,
headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
)
diff --git a/feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/CreateQuizDataToDomainMapperTest.kt
similarity index 96%
rename from feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt
rename to feature/interview-trainer/impl/src/test/java/test/CreateQuizDataToDomainMapperTest.kt
index 59282f55..c9acfba3 100644
--- a/feature/interview-trainer/impl/src/test/java/test/CreateQuizMapperTest.kt
+++ b/feature/interview-trainer/impl/src/test/java/test/CreateQuizDataToDomainMapperTest.kt
@@ -10,20 +10,9 @@ import ru.yeahub.network_api.models.GetSpecializationResponse
import ru.yeahub.network_api.models.GetSpecializationsResponse
import ru.yeahub.test.TestArgumentsProvider
-class CreateQuizMapperTest {
+class CreateQuizDataToDomainMapperTest {
private val toDomainMapper = CreateQuizDataToDomainMapper()
- //CreateQuizDataToDomainMapper параметризированный тест
- class ArgumentsProvider :
- TestArgumentsProvider() {
- override fun testCases(): List = listOf(
- SpecializationSelectionDataToDomainMapperTestCase(
- dataToTest = SpecializationExampleDataClasses.defaultSpecialListResponse,
- expectedResult = SpecializationExampleDataClasses.defaultDomainSpecialListResponse
- )
- )
- }
-
@ParameterizedTest
@ArgumentsSource(ArgumentsProvider::class)
fun specializationSelectionDataToDomainMapperTestCase(
@@ -79,4 +68,14 @@ class CreateQuizMapperTest {
val dataToTest: GetSpecializationsResponse,
val expectedResult: DomainSpecializationListResponse,
)
+
+ class ArgumentsProvider :
+ TestArgumentsProvider() {
+ override fun testCases(): List = listOf(
+ SpecializationSelectionDataToDomainMapperTestCase(
+ dataToTest = SpecializationExampleDataClasses.defaultSpecialListResponse,
+ expectedResult = SpecializationExampleDataClasses.defaultDomainSpecialListResponse
+ )
+ )
+ }
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
new file mode 100644
index 00000000..a041a7d3
--- /dev/null
+++ b/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
@@ -0,0 +1,89 @@
+package test
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreenMapper
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
+import ru.yeahub.test.TestArgumentsProvider
+
+class CreateQuizScreenMapperTest {
+
+ @ParameterizedTest
+ @ArgumentsSource(ArgumentsProvider::class)
+ fun getScreenStateTest(
+ testCase: CreateQuizScreenMapperTestCase,
+ ) {
+ val result = CreateQuizScreenMapper.getScreenState(
+ specializations = testCase.specializations,
+ selectedSpecializationId = testCase.selectedSpecializationId,
+ questionsCount = testCase.questionsCount
+ )
+ Assertions.assertEquals(testCase.expectedResult, result)
+ }
+
+ object ScreenMapperExampleDataClasses {
+ val defaultDomainSpecial = DomainSpecialization(
+ id = 0L,
+ title = "default title num 0"
+ )
+
+ val defaultDomainSpecialWithImage = DomainSpecialization(
+ id = 1L,
+ title = "default title num 1"
+ )
+
+ val defaultDomainSpecialList = listOf(
+ defaultDomainSpecial,
+ defaultDomainSpecialWithImage
+ )
+
+ val defaultVoSpecial = CreateQuizState.Loaded.VoSpecialization(
+ id = 0L,
+ title = "default title num 0"
+ )
+
+ val defaultVoSpecialWithImage = CreateQuizState.Loaded.VoSpecialization(
+ id = 1L,
+ title = "default title num 1"
+ )
+
+ val defaultLoadedState = CreateQuizState.Loaded(
+ specializations = listOf(defaultVoSpecial, defaultVoSpecialWithImage),
+ selectedSpecializationId = 0L,
+ questionsCount = 10
+ )
+
+ val loadedStateWithDifferentSelection = CreateQuizState.Loaded(
+ specializations = listOf(defaultVoSpecial, defaultVoSpecialWithImage),
+ selectedSpecializationId = 1L,
+ questionsCount = 5
+ )
+ }
+
+ data class CreateQuizScreenMapperTestCase(
+ val specializations: List,
+ val selectedSpecializationId: Long,
+ val questionsCount: Int,
+ val expectedResult: CreateQuizState.Loaded,
+ )
+
+ class ArgumentsProvider :
+ TestArgumentsProvider() {
+ override fun testCases(): List = listOf(
+ CreateQuizScreenMapperTestCase(
+ specializations = ScreenMapperExampleDataClasses.defaultDomainSpecialList,
+ selectedSpecializationId = 0L,
+ questionsCount = 10,
+ expectedResult = ScreenMapperExampleDataClasses.defaultLoadedState
+ ),
+ CreateQuizScreenMapperTestCase(
+ specializations = ScreenMapperExampleDataClasses.defaultDomainSpecialList,
+ selectedSpecializationId = 1L,
+ questionsCount = 5,
+ expectedResult = ScreenMapperExampleDataClasses.loadedStateWithDifferentSelection
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/selection-specializations/impl/src/test/java/test/SpecializationExampleDataClasses.kt b/feature/selection-specializations/impl/src/test/java/test/SpecializationExampleDataClasses.kt
deleted file mode 100644
index 38b8d9a8..00000000
--- a/feature/selection-specializations/impl/src/test/java/test/SpecializationExampleDataClasses.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package test
-
-import ru.yeahub.network_api.models.GetSpecializationResponse
-import ru.yeahub.network_api.models.GetSpecializationsResponse
-import ru.yeahub.selection_specializations.impl.domain.DomainSpecilialization
-import ru.yeahub.selection_specializations.impl.domain.DomainSpecilializationListResponse
-import ru.yeahub.selection_specializations.impl.presentation.VoSpecilialization
-
-object SpecializationExampleDataClasses {
- val defaultSpecialResponse = GetSpecializationResponse(
- id = 0L,
- title = "default title num 0",
- description = "default description",
- imageSrc = null,
- createdAt = "01.01.1970",
- updatedAt = "01.01.1970"
- )
-
- val defaultSpecialResponseWithImage = GetSpecializationResponse(
- id = 1L,
- title = "default title num 1",
- description = "default description",
- imageSrc = "some image url",
- createdAt = "01.01.1970",
- updatedAt = "01.01.1970"
- )
-
- val defaultDomainSpecial = DomainSpecilialization(
- id = 0L,
- title = "default title num 0"
- )
-
- val defaultDomainSpecialWithImage = DomainSpecilialization(
- id = 1L,
- title = "default title num 1"
- )
-
- val defaultSpecialListResponse = GetSpecializationsResponse(
- page = 1L,
- limit = 10L,
- data = listOf(defaultSpecialResponse, defaultSpecialResponseWithImage),
- total = 2L
- )
-
- val defaultDomainSpecialListResponse = DomainSpecilializationListResponse(
- page = 1L,
- limit = 10L,
- data = listOf(defaultDomainSpecial, defaultDomainSpecialWithImage),
- total = 2L
- )
-
- val defaultVoSpecialization = VoSpecilialization(
- id = 0,
- title = "default title num 0"
- )
-
- val defaultVoSpecializationWithImage = VoSpecilialization(
- id = 1,
- title = "default title num 1"
- )
-}
-
diff --git a/feature/selection-specializations/impl/src/test/java/test/SpecializationSelectionMapperTest.kt b/feature/selection-specializations/impl/src/test/java/test/SpecializationSelectionMapperTest.kt
deleted file mode 100644
index 14d957a6..00000000
--- a/feature/selection-specializations/impl/src/test/java/test/SpecializationSelectionMapperTest.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package test
-
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ArgumentsSource
-import ru.yeahub.network_api.models.GetSpecializationResponse
-import ru.yeahub.network_api.models.GetSpecializationsResponse
-import ru.yeahub.selection_specializations.impl.data.SpecializationSelectionDataToDomainMapper
-import ru.yeahub.selection_specializations.impl.domain.DomainSpecilialization
-import ru.yeahub.selection_specializations.impl.domain.DomainSpecilializationListResponse
-import ru.yeahub.selection_specializations.impl.presentation.SpecializationSelectionScreenMapper.getScreenState
-import ru.yeahub.selection_specializations.impl.presentation.VoSpecilialization
-import ru.yeahub.test.TestArgumentsProvider
-import test.SpecializationExampleDataClasses.defaultDomainSpecial
-import test.SpecializationExampleDataClasses.defaultDomainSpecialListResponse
-import test.SpecializationExampleDataClasses.defaultDomainSpecialWithImage
-import test.SpecializationExampleDataClasses.defaultSpecialListResponse
-import test.SpecializationExampleDataClasses.defaultSpecialResponse
-import test.SpecializationExampleDataClasses.defaultSpecialResponseWithImage
-import test.SpecializationExampleDataClasses.defaultVoSpecialization
-import test.SpecializationExampleDataClasses.defaultVoSpecializationWithImage
-
-class SpecializationSelectionMapperTest {
- private val toDomainMapper = SpecializationSelectionDataToDomainMapper()
-
- class ArgumentsProvider1 : TestArgumentsProvider(){
- override fun testCases(): List = listOf(
- SpecializationSelectionDataToDomainMapperTestCase1(
- dataToTest = defaultSpecialResponse,
- expectedResult = defaultDomainSpecial
- ),
- SpecializationSelectionDataToDomainMapperTestCase1(
- dataToTest = defaultSpecialResponseWithImage,
- expectedResult = defaultDomainSpecialWithImage
- )
- )
- }
-
- class ArgumentsProvider2 : TestArgumentsProvider(){
- override fun testCases(): List = listOf(
- SpecializationSelectionDataToDomainMapperTestCase2(
- dataToTest = defaultSpecialListResponse,
- expectedResult = defaultDomainSpecialListResponse
- )
- )
- }
-
- class ArgumentsProvider3 : TestArgumentsProvider(){
- override fun testCases(): List = listOf(
- SpecializationSelectionDomainToVoMapperTestCase(
- dataToTest = listOf(defaultDomainSpecial, defaultDomainSpecialWithImage),
- expectedResult = listOf(defaultVoSpecialization, defaultVoSpecializationWithImage)
- )
- )
- }
-
- @ParameterizedTest
- @ArgumentsSource(ArgumentsProvider1::class)
- fun specializationSelectionDataToDomainMapperTestCase1(
- testCase1: SpecializationSelectionDataToDomainMapperTestCase1
- ){
- val result = toDomainMapper.dataToDomain(testCase1.dataToTest)
- assertEquals(testCase1.expectedResult, result)
- }
-
- @ParameterizedTest
- @ArgumentsSource(ArgumentsProvider2::class)
- fun specializationSelectionDataToDomainMapperTestCase2(
- testCase2: SpecializationSelectionDataToDomainMapperTestCase2
- ){
- val result = toDomainMapper.dataListToDomainList(testCase2.dataToTest)
- assertEquals(testCase2.expectedResult, result)
- }
-
- @ParameterizedTest
- @ArgumentsSource(ArgumentsProvider3::class)
- fun specializationSelectionDomainToVoMapperTestCase(
- testCase: SpecializationSelectionDomainToVoMapperTestCase
- ){
- val result = getScreenState(testCase.dataToTest)
- assertEquals(testCase.expectedResult, result)
- }
-}
-
-data class SpecializationSelectionDataToDomainMapperTestCase1(
- val dataToTest: GetSpecializationResponse,
- val expectedResult: DomainSpecilialization
-)
-
-data class SpecializationSelectionDataToDomainMapperTestCase2(
- val dataToTest: GetSpecializationsResponse,
- val expectedResult: DomainSpecilializationListResponse
-)
-
-data class SpecializationSelectionDomainToVoMapperTestCase(
- val dataToTest: List,
- val expectedResult: List
-)
-
From 017c4fe4e0d65c4995e3b802603339dc187cfd1f Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Thu, 19 Feb 2026 15:29:32 +0300
Subject: [PATCH 04/28] =?UTF-8?q?=20[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-71:=20=D0=A7=D0=B5=D1=82=D0=B2=D0=B5=D1=80=D1=82?=
=?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF=20CreateQuizScreen.=20?=
=?UTF-8?q?=D0=9D=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D1=8F=20+=20En?=
=?UTF-8?q?tryPoint=20(#102)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-71: Добавление нужных текстов для entry-point'а
* ANDR-71: Добавление сущности тренажера в бизнес логике главного экрана
* ANDR-71: Добавление обработки команды по тренажеру
* ANDR-71: Добавлена функция обработки навигации к тренажеру и передается в параметр главного экрана
* ANDR-71: Тест Превью с кнопкой тренажера
* ANDR-71: Использование лямбды навигации для отправки комманд
* ANDR-71: Изменена внешняя функция навигации-входа в фичу тренажера
* ANDR-71: Добавлена реализация InterviewTrainerFeatureImpl. Внутри регистрация графа, обработка навигации назад и обработка навигации к экрану тренировки
* ANDR-71: правки по ktlint'у
* ANDR-71: ScreenMapper теперь класс
* ANDR-71: Константы названий экранов интервью-тренажера
* ANDR-71: Реворк навигации
* ANDR-71: Улучшение экрана. Подключение вьюмодели и команд
* ANDR-71: Вторичные Модули DI
* ANDR-71: Вторичный маппер DI модуль
* ANDR-71: Основной DI модуль экрана
* ANDR-71: Подключение модуля тренажера во все приложение
* ANDR-71: Подключение KOIN модуля
* ANDR-71: Исправлен DI модуль для юзкейса
* ANDR-71: Добавил экрану скролл
* ANDR-71: Навигация с главного экрана на первый экран тренажера (CreateQuizScreen)
* ANDR-71: Создание путей для экрана тренажера
* ANDR-71: Убран хардкод путь экрана CreateQuiz
* ANDR-71: Исправлен класс теста
* ANDR-71: проставлен is
* ANDR-71: добавлен строковой ресурс названия 1 экрана тренажера для прокидывания по навигации
* ANDR-71: используем pathManager для навигации к экрану тренировки
* ANDR-71: handleCommand приватная
* ANDR-71: перегрузка метода получения состояния
* ANDR-71: переименован юзкейс
* ANDR-71: Рефактор DI. Все модули объединены в один
* ANDR-71: коммент к DI
* ANDR-71: Подключение Immutable Collections
* ANDR-71: Удаление наружнего апи интерфейса
* ANDR-71: Оптимизация навигации
* ANDR-71: Навигация теперь принимает не строку, а число айди ресурса
* ANDR-71: исправлены тесты
* ANDR-71: В комментарии вставлены TODO()
---
app/build.gradle.kts | 2 +
app/src/main/java/ru/yeahub/Application.kt | 4 +-
.../ru/yeahub/navigation_api/FeatureRoute.kt | 4 +
core/ui/src/main/res/values/strings.xml | 3 +
.../impl/QuestionMainFeatureImpl.kt | 37 ++++++-
.../intents/QuestionMainScreenCommand.kt | 1 +
.../mapper/QuestionMainScreenMapper.kt | 7 ++
.../model/QuestionMainItemType.kt | 1 +
.../presentation/view/QuestionsMainScreen.kt | 13 ++-
.../viewmodel/QuestionMainViewModel.kt | 4 +
.../impl/src/main/res/values/strings.xml | 4 +
.../api/InterviewTrainerApi.kt | 21 ----
.../interview-trainer/impl/build.gradle.kts | 6 +-
.../impl/InterviewTrainerFeatureImpl.kt | 97 ++++++++++++++++++-
.../impl/createQuiz/di/CreateQuizModule.kt | 38 ++++++++
...se.kt => GetSpecializationsListUseCase.kt} | 2 +-
.../GetSpecializationsListUseCaseImpl.kt | 2 +-
.../presentation/CreateQuizScreenMapper.kt | 9 +-
.../presentation/CreateQuizState.kt | 3 +-
.../presentation/CreateQuizViewModel.kt | 6 +-
.../impl/createQuiz/ui/CreateQuizScreen.kt | 53 +++++++---
.../java/test/CreateQuizScreenMapperTest.kt | 7 +-
22 files changed, 272 insertions(+), 52 deletions(-)
create mode 100644 feature/example-home/impl/src/main/res/values/strings.xml
delete mode 100644 feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/{GetSpecializationsUseCase.kt => GetSpecializationsListUseCase.kt} (79%)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ca5114c6..98a588b1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -115,6 +115,8 @@ dependencies {
implementation(project(":feature:questions-or-collections:impl"))
implementation(project(":feature:public-collections:impl"))
implementation(project(":feature:selection-specializations:impl"))
+ implementation(project(":feature:interview-trainer:api"))
+ implementation(project(":feature:interview-trainer:impl"))
}
tasks.withType {
diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt
index 792c54b3..f36bfecf 100644
--- a/app/src/main/java/ru/yeahub/Application.kt
+++ b/app/src/main/java/ru/yeahub/Application.kt
@@ -7,6 +7,7 @@ import ru.yeahub.detail_question.impl.di.detailQuestionFeatureModule
import ru.yeahub.example_details.impl.detailsFeatureModule
import ru.yeahub.example_home.impl.data.di.questionsMainFeatureModule
import ru.yeahub.example_profile.impl.profileFeatureModule
+import ru.yeahub.interview_trainer.impl.createQuiz.di.createQuizModule
import ru.yeahub.navigation_impl.navigationPathModule
import ru.yeahub.network_impl.networkModule
import ru.yeahub.public_collections.impl.di.CollectionsFeatureModule
@@ -53,7 +54,8 @@ class Application : Application() {
CollectionsFeatureModule,
detailQuestionFeatureModule,
collectionsAndQuestionsFeatureModule,
- specializationFeatureModule
+ specializationFeatureModule,
+ createQuizModule
)
}
// проверка, что модули загружены
diff --git a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
index bccd38fb..4d23aba2 100644
--- a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
+++ b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt
@@ -65,5 +65,9 @@ object FeatureRoute {
object InterviewTrainerFeature {
const val FEATURE_NAME = "interview_trainer"
+
+ const val CREATE_QUIZ_SCREEN_NAME = "create_quiz"
+ const val INTERVIEW_QUIZ_SCREEN_NAME = "interview_quiz"
+ const val INTERVIEW_QUIZ_RESULT_SCREEN_NAME = "interview_quiz_result"
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index 774e6c8b..1b7c1a34 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -24,4 +24,7 @@
УПС!
Назад
Не удалось загрузить данные
+
+ Интервью тренажер
+ Улучшите свои знания перед собеседованием
\ No newline at end of file
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
index 2bc10887..e9d85fb8 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
@@ -4,6 +4,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
+import ru.yeahub.core_utils.common.TextOrResource
import ru.yeahub.example_home.impl.presentation.view.QuestionsMainScreen
import ru.yeahub.navigation_api.FeatureApi
import ru.yeahub.navigation_api.FeatureRoute
@@ -28,7 +29,7 @@ class QuestionMainFeatureImpl : FeatureApi {
navGraphBuilder: NavGraphBuilder,
navController: NavHostController,
pathManager: NavigationPathManager,
- modifier: Modifier
+ modifier: Modifier,
) {
val currentPath = pathManager.getCurrentPath()
Timber.d("HomeFeatureImpl registerGraph: currentPath: $currentPath")
@@ -50,6 +51,9 @@ class QuestionMainFeatureImpl : FeatureApi {
},
onNavigateToCollections = {
handleCollectionsNavigation(pathManager, navController)
+ },
+ onNavigateToInterviewTrainer = {
+ handleInterviewTrainerNavigation(pathManager, navController)
}
)
}
@@ -60,7 +64,7 @@ class QuestionMainFeatureImpl : FeatureApi {
*/
private fun handleQuestionsNavigation(
pathManager: NavigationPathManager,
- navController: NavHostController
+ navController: NavHostController,
) {
// Сбрасываем текущий путь на корневую фичу
pathManager.setCurrentPath(FeatureRoute.QuestionsFeature.FEATURE_NAME)
@@ -83,7 +87,7 @@ class QuestionMainFeatureImpl : FeatureApi {
*/
private fun handleCollectionsNavigation(
pathManager: NavigationPathManager,
- navController: NavHostController
+ navController: NavHostController,
) {
// Сбрасываем текущий путь на корневую фичу
pathManager.setCurrentPath(FeatureRoute.CollectionsFeature.FEATURE_NAME)
@@ -100,4 +104,31 @@ class QuestionMainFeatureImpl : FeatureApi {
restoreState = true
}
}
+
+ /**
+ * Обработка навигации к интервью тренажеру.
+ */
+ private fun handleInterviewTrainerNavigation(
+ pathManager: NavigationPathManager,
+ navController: NavHostController,
+ ) {
+ val titleTopAppBarResId = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+
+ // Сбрасываем текущий путь на корневую фичу
+ pathManager.setCurrentPath(FeatureRoute.InterviewTrainerFeature.FEATURE_NAME)
+
+ val createQuizPath = pathManager.createChildPath(
+ featureName = FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME
+ ) + "/" + titleTopAppBarResId.resource
+
+ Timber.d("HomeFeatureImpl handleInterviewTrainerNavigation: Navigating to: $createQuizPath")
+
+ navController.navigate(createQuizPath) {
+ popUpTo(navController.graph.startDestinationId) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
}
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/intents/QuestionMainScreenCommand.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/intents/QuestionMainScreenCommand.kt
index 99d2037e..5b55dd91 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/intents/QuestionMainScreenCommand.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/intents/QuestionMainScreenCommand.kt
@@ -3,4 +3,5 @@ package ru.yeahub.example_home.impl.presentation.intents
sealed class QuestionMainScreenCommand {
object NavigateToBaseQuestions : QuestionMainScreenCommand()
object NavigateToCollections : QuestionMainScreenCommand()
+ object NavigateToInterviewTrainer : QuestionMainScreenCommand()
}
\ No newline at end of file
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
index f8b1470f..5e435fbf 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
@@ -14,6 +14,13 @@ class QuestionMainScreenMapper {
description = TextOrResource.Resource(R.string.base_questions_description),
imageRes = R.drawable.icon_base_question
),
+ // TODO("imageRes тренажера потом изменить на нормальный")
+ QuestionMainUiModel(
+ type = QuestionMainItemType.InterviewTrainer,
+ title = TextOrResource.Resource(R.string.interview_trainer_title),
+ description = TextOrResource.Resource(R.string.interview_trainer_description),
+ imageRes = R.drawable.question_square
+ ),
QuestionMainUiModel(
type = QuestionMainItemType.Collections,
title = TextOrResource.Resource(R.string.collections_title),
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/model/QuestionMainItemType.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/model/QuestionMainItemType.kt
index dbb2aa25..4c6e20c9 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/model/QuestionMainItemType.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/model/QuestionMainItemType.kt
@@ -3,4 +3,5 @@ package ru.yeahub.example_home.impl.presentation.model
sealed class QuestionMainItemType {
object BaseQuestions : QuestionMainItemType()
object Collections : QuestionMainItemType()
+ object InterviewTrainer : QuestionMainItemType()
}
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/view/QuestionsMainScreen.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/view/QuestionsMainScreen.kt
index f433bac6..1898dc7f 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/view/QuestionsMainScreen.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/view/QuestionsMainScreen.kt
@@ -37,7 +37,8 @@ import ru.yeahub.ui.R
fun QuestionsMainScreen(
onBackClick: () -> Unit,
onNavigateToBaseQuestions: () -> Unit,
- onNavigateToCollections: () -> Unit
+ onNavigateToCollections: () -> Unit,
+ onNavigateToInterviewTrainer: () -> Unit,
) {
val viewModel: QuestionMainViewModel = koinViewModel()
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -46,6 +47,7 @@ fun QuestionsMainScreen(
when (command) {
is QuestionMainScreenCommand.NavigateToBaseQuestions -> onNavigateToBaseQuestions()
is QuestionMainScreenCommand.NavigateToCollections -> onNavigateToCollections()
+ is QuestionMainScreenCommand.NavigateToInterviewTrainer -> onNavigateToInterviewTrainer()
}
}
@@ -60,7 +62,7 @@ fun QuestionsMainScreen(
fun QuestionsMainScreenContent(
state: QuestionMainScreenState,
onItemClick: (QuestionMainUiModel) -> Unit,
- onBackClick: () -> Unit
+ onBackClick: () -> Unit,
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
@@ -193,6 +195,13 @@ val stateWithContent = QuestionMainScreenState.Content(
description = TextOrResource.Resource(R.string.base_questions_description),
imageRes = R.drawable.icon_base_question
),
+ // TODO("imageRes тренажера потом изменить на нормальный")
+ QuestionMainUiModel(
+ type = QuestionMainItemType.InterviewTrainer,
+ title = TextOrResource.Resource(R.string.interview_trainer_title),
+ description = TextOrResource.Resource(R.string.interview_trainer_description),
+ imageRes = R.drawable.question_square
+ ),
QuestionMainUiModel(
type = QuestionMainItemType.Collections,
title = TextOrResource.Resource(R.string.collections_title),
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
index 2b261a43..ba3b7c6a 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
@@ -45,6 +45,10 @@ class QuestionMainViewModel(
)
QuestionMainItemType.Collections ->
_command.emit(QuestionMainScreenCommand.NavigateToCollections)
+
+ QuestionMainItemType.InterviewTrainer -> {
+ _command.emit(QuestionMainScreenCommand.NavigateToInterviewTrainer)
+ }
}
}
}
diff --git a/feature/example-home/impl/src/main/res/values/strings.xml b/feature/example-home/impl/src/main/res/values/strings.xml
new file mode 100644
index 00000000..a89ccdc5
--- /dev/null
+++ b/feature/example-home/impl/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Подготовка
+
\ No newline at end of file
diff --git a/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
deleted file mode 100644
index b98d29ed..00000000
--- a/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/InterviewTrainerApi.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package ru.yeahub.interview_trainer.api
-
-import androidx.compose.runtime.Composable
-
-/**
- * API интерфейс для экрана тренажера собеседований.
- *
- * Демонстрирует:
- * - Передачу параметров между экранами
- */
-interface InterviewTrainerApi {
- /**
- * Экран тренажера собеседований.
- *
- * @param onBackClick Действие при нажатии кнопки "Назад"
- */
- @Composable
- fun InterviewTrainerScreen(
- onBackClick: () -> Unit,
- )
-}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts
index 1d5e92b4..4ab71943 100644
--- a/feature/interview-trainer/impl/build.gradle.kts
+++ b/feature/interview-trainer/impl/build.gradle.kts
@@ -50,9 +50,13 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.runtime.android)
+ // Блики
implementation(libs.compose.shimmer)
- //KOIN
+ // Неизменяемые коллекции
+ implementation(libs.immutable.collections)
+
+ // KOIN
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
index 1c9a29f4..50eb3940 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
@@ -3,9 +3,18 @@ package ru.yeahub.interview_trainer.impl
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizResult
+import ru.yeahub.interview_trainer.impl.createQuiz.ui.CreateQuizScreen
import ru.yeahub.navigation_api.FeatureApi
import ru.yeahub.navigation_api.FeatureRoute
import ru.yeahub.navigation_api.NavigationPathManager
+import timber.log.Timber
+
+private const val TITLE_TOP_APP_BAR = "title"
+private const val NOT_FOUND_NUMBER = 404
class InterviewTrainerFeatureImpl : FeatureApi {
override fun getFeatureName(): String = FeatureRoute.InterviewTrainerFeature.FEATURE_NAME
@@ -16,6 +25,92 @@ class InterviewTrainerFeatureImpl : FeatureApi {
pathManager: NavigationPathManager,
modifier: Modifier,
) {
- TODO("Not yet implemented")
+ //Регистрируем базовый путь фичи (interview_trainer)
+ pathManager.registerFeaturePath(featureName = getFeatureName(), basePath = getFeatureName())
+
+ val featurePath = pathManager.getFeaturePath(getFeatureName()) ?: getFeatureName()
+
+ //Регистрируем путь экрана создания тренировки (interview_trainer/create_quiz/{titleId})
+ val createQuizRoute =
+ "$featurePath/${FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME}/{$TITLE_TOP_APP_BAR}"
+
+ Timber.d("InterviewTrainerFeatureImpl registerGraph: currentPath: $createQuizRoute")
+
+ navGraphBuilder.composable(
+ route = createQuizRoute,
+ arguments = listOf(
+ navArgument(TITLE_TOP_APP_BAR) {
+ type = NavType.IntType
+ }
+ )
+ ) { backStackEntry ->
+ val titleTopAppBarResId =
+ backStackEntry.arguments?.getInt(TITLE_TOP_APP_BAR) ?: NOT_FOUND_NUMBER
+
+ // Вынос в отдельную переменную для оптимизации (чтоб не пересоздавать лямбду)
+ val onResult = { result: CreateQuizResult ->
+ when (result) {
+ is CreateQuizResult.NavigateBack -> handleBackNavigation(
+ pathManager = pathManager,
+ navController = navController
+ )
+
+ is CreateQuizResult.NavigateToInterviewQuizScreen -> handleQuizNavigation(
+ pathManager = pathManager,
+ navController = navController,
+ featurePath = featurePath,
+ titleTopAppBarResId = titleTopAppBarResId,
+ specializationId = result.specializationId.toString(),
+ questionsCount = result.questionCount.toString()
+ )
+ }
+ }
+ CreateQuizScreen(onResult = onResult, titleTopAppBarResId = titleTopAppBarResId)
+ }
+ }
+
+ /**
+ * Обработка навигации назад.
+ */
+ private fun handleBackNavigation(
+ pathManager: NavigationPathManager,
+ navController: NavHostController,
+ ) {
+ val parentPath = pathManager.getParentPath()
+ Timber.d("InterviewTrainerFeatureImpl handleBackNavigation: Navigating to parent: $parentPath")
+
+ pathManager.setCurrentPath(parentPath)
+
+ if (parentPath.isEmpty()) {
+ navController.navigateUp()
+ } else {
+ navController.navigate(parentPath) {
+ popUpTo(parentPath) {
+ inclusive = true
+ }
+ }
+ }
+ }
+
+ /**
+ * Обработка навигации к экрану тренировки (InterviewQuizScreen).
+ */
+ private fun handleQuizNavigation(
+ pathManager: NavigationPathManager,
+ navController: NavHostController,
+ featurePath: String,
+ titleTopAppBarResId: Int,
+ specializationId: String,
+ questionsCount: String,
+ ) {
+ //Регистрируем путь экрана тренировки (interview_trainer/create_quiz/{titleId})
+ val interviewQuizRoute = featurePath + "/" +
+ FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_SCREEN_NAME + "/" +
+ "$titleTopAppBarResId/$specializationId/$questionsCount"
+
+ Timber.d("InterviewTrainerFeatureImpl registerGraph: $interviewQuizRoute")
+
+ navController.navigate(interviewQuizRoute)
+ pathManager.setCurrentPath(interviewQuizRoute)
}
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
new file mode 100644
index 00000000..65022d24
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
@@ -0,0 +1,38 @@
+package ru.yeahub.interview_trainer.impl.createQuiz.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.bind
+import org.koin.dsl.module
+import ru.yeahub.interview_trainer.impl.InterviewTrainerFeatureImpl
+import ru.yeahub.interview_trainer.impl.createQuiz.data.CreateQuizDataToDomainMapper
+import ru.yeahub.interview_trainer.impl.createQuiz.data.CreateQuizRepositoryImpl
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.CreateQuizRepositoryApi
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsListUseCase
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsListUseCaseImpl
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreenMapper
+import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
+import ru.yeahub.navigation_api.FeatureApi
+
+val createQuizModule = module {
+ // FeatureImpl для реализации навигации фичи
+ single { InterviewTrainerFeatureImpl() } bind FeatureApi::class
+
+ // Мапперы
+ single { CreateQuizScreenMapper() }
+ single { CreateQuizDataToDomainMapper() }
+
+ // Репозиторий
+ single {
+ CreateQuizRepositoryImpl(apiService = get(), mapper = get())
+ }
+
+ // Юзкейсы
+ single {
+ GetSpecializationsListUseCaseImpl(repository = get())
+ }
+
+ // ВьюМоделька экрана
+ viewModel {
+ CreateQuizViewModel(getSpecializationsListUseCase = get(), screenMapper = get())
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCase.kt
similarity index 79%
rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt
rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCase.kt
index 4171b335..d559833c 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsUseCase.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCase.kt
@@ -1,5 +1,5 @@
package ru.yeahub.interview_trainer.impl.createQuiz.domain
-interface GetSpecializationsUseCase {
+interface GetSpecializationsListUseCase {
suspend operator fun invoke(request: SpecializationsRequest): DomainSpecializationListResponse
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
index ac3e52f8..549c1ec2 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/domain/GetSpecializationsListUseCaseImpl.kt
@@ -2,7 +2,7 @@ package ru.yeahub.interview_trainer.impl.createQuiz.domain
class GetSpecializationsListUseCaseImpl(
private val repository: CreateQuizRepositoryApi,
-) : GetSpecializationsUseCase {
+) : GetSpecializationsListUseCase {
override suspend fun invoke(
request: SpecializationsRequest,
): DomainSpecializationListResponse = repository.getSpecializationsList(request = request)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
index 1abd7353..9a578480 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
@@ -1,8 +1,9 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
+import kotlinx.collections.immutable.toImmutableList
import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
-object CreateQuizScreenMapper {
+class CreateQuizScreenMapper() {
fun getScreenState(
specializations: List,
@@ -14,8 +15,12 @@ object CreateQuizScreenMapper {
id = domainSpec.id,
title = domainSpec.title
)
- },
+ }.toImmutableList(),
selectedSpecializationId = selectedSpecializationId,
questionsCount = questionsCount
)
+
+ fun getScreenState(
+ throwable: Throwable,
+ ): CreateQuizState = CreateQuizState.Error(throwable)
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
index 07a2d3c6..74b00c64 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
@@ -1,13 +1,14 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
sealed interface CreateQuizState {
//Изначальный
data object Loading : CreateQuizState
data class Loaded(
- val specializations: List,
+ val specializations: ImmutableList,
val selectedSpecializationId: Long,
val questionsCount: Int,
) : CreateQuizState {
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
index 1a163104..f0f4ccd6 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -10,11 +10,11 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.yeahub.core_utils.BaseViewModel
-import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsUseCase
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsListUseCase
import ru.yeahub.interview_trainer.impl.createQuiz.domain.SpecializationsRequest
open class CreateQuizViewModel(
- private val getSpecializationsListUseCase: GetSpecializationsUseCase,
+ private val getSpecializationsListUseCase: GetSpecializationsListUseCase,
private val screenMapper: CreateQuizScreenMapper,
) : BaseViewModel() {
@@ -45,7 +45,7 @@ open class CreateQuizViewModel(
fun onEvent(event: CreateQuizEvent) {
when (event) {
- CreateQuizEvent.OnBackClick -> onBackClick()
+ is CreateQuizEvent.OnBackClick -> onBackClick()
is CreateQuizEvent.OnPlusQuestionClick -> incrementQuestionsCount(
questionsCount = event.questionsCount
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index 8ef23837..9a93809d 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
@@ -36,9 +38,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import org.koin.androidx.compose.koinViewModel
import ru.yeahub.core_ui.component.ErrorScreen
import ru.yeahub.core_ui.component.PrimaryButton
import ru.yeahub.core_ui.component.SkillButton
@@ -52,7 +58,7 @@ import ru.yeahub.core_utils.common.observe
import ru.yeahub.interview_trainer.impl.R
import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecializationListResponse
-import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsUseCase
+import ru.yeahub.interview_trainer.impl.createQuiz.domain.GetSpecializationsListUseCase
import ru.yeahub.interview_trainer.impl.createQuiz.domain.SpecializationsRequest
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizCommand
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizEvent
@@ -65,23 +71,46 @@ private val FIGMA_HORIZONTAL_PADDING = 16.dp
private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
+@Composable
+fun CreateQuizScreen(
+ onResult: (CreateQuizResult) -> Unit,
+ titleTopAppBarResId: Int,
+) {
+ val viewModel: CreateQuizViewModel = koinViewModel()
+
+ val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+
+ HandleCommand(
+ commandFlow = viewModel.commands,
+ onResult = { result -> onResult(result) }
+ )
+
+ ScreenUI(
+ state = screenState,
+ onEvent = viewModel::onEvent,
+ titleTopAppBar = TextOrResource.Resource(titleTopAppBarResId)
+ )
+}
+
@Composable
private fun ScreenUI(
state: CreateQuizState,
onEvent: (CreateQuizEvent) -> Unit,
- headerText: TextOrResource,
+ titleTopAppBar: TextOrResource,
) {
Scaffold(
containerColor = colors.black10,
topBar = {
TopAppBarWithBottomBorder(
- title = headerText,
+ title = titleTopAppBar,
onBackClick = { onEvent(CreateQuizEvent.OnBackClick) }
)
}
) { paddingValues ->
Box(
- modifier = Modifier.padding(paddingValues)
+ modifier = Modifier
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
) {
when (state) {
CreateQuizState.Loading -> CreateQuizLoading()
@@ -126,7 +155,7 @@ private fun ScreenUI(
}
@Composable
-fun HandleCommand(
+private fun HandleCommand(
commandFlow: Flow,
onResult: (CreateQuizResult) -> Unit,
) {
@@ -145,7 +174,7 @@ fun HandleCommand(
@Composable
private fun BaseCreateQuizScreen(
- specializations: List,
+ specializations: ImmutableList,
selectedSpecializationId: Long,
questionsCount: Int,
onSpecializationClick: (id: Long) -> Unit,
@@ -198,7 +227,7 @@ private fun BaseCreateQuizScreen(
@Composable
private fun ChooseSpecializationBlock(
- specializations: List,
+ specializations: ImmutableList,
selectedSpecializationId: Long,
context: Context,
onSpecializationClick: (Long) -> Unit,
@@ -351,7 +380,7 @@ private fun StartQuizButton(
}
}
-val specializations = listOf(
+val specializations = persistentListOf(
CreateQuizState.Loaded.VoSpecialization(
id = 11,
title = "Frontend"
@@ -409,7 +438,7 @@ fun CreateQuizScreenPreview(
ScreenUI(
state = state,
onEvent = { },
- headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
)
}
@@ -420,7 +449,7 @@ fun DynamicPreviewUI() {
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
- val mockUseCase = object : GetSpecializationsUseCase {
+ val mockUseCase = object : GetSpecializationsListUseCase {
override suspend fun invoke(
request: SpecializationsRequest,
): DomainSpecializationListResponse {
@@ -433,7 +462,7 @@ fun DynamicPreviewUI() {
}
val mockViewModel = viewModelCreator {
- CreateQuizViewModel(mockUseCase, CreateQuizScreenMapper)
+ CreateQuizViewModel(mockUseCase, CreateQuizScreenMapper())
}
val mockState by mockViewModel.screenState.collectAsState()
@@ -458,7 +487,7 @@ fun DynamicPreviewUI() {
ScreenUI(
state = mockState,
onEvent = mockViewModel::onEvent,
- headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
)
}
}
diff --git a/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
index a041a7d3..f57a2b85 100644
--- a/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
+++ b/feature/interview-trainer/impl/src/test/java/test/CreateQuizScreenMapperTest.kt
@@ -1,5 +1,6 @@
package test
+import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource
@@ -15,7 +16,7 @@ class CreateQuizScreenMapperTest {
fun getScreenStateTest(
testCase: CreateQuizScreenMapperTestCase,
) {
- val result = CreateQuizScreenMapper.getScreenState(
+ val result = CreateQuizScreenMapper().getScreenState(
specializations = testCase.specializations,
selectedSpecializationId = testCase.selectedSpecializationId,
questionsCount = testCase.questionsCount
@@ -50,13 +51,13 @@ class CreateQuizScreenMapperTest {
)
val defaultLoadedState = CreateQuizState.Loaded(
- specializations = listOf(defaultVoSpecial, defaultVoSpecialWithImage),
+ specializations = persistentListOf(defaultVoSpecial, defaultVoSpecialWithImage),
selectedSpecializationId = 0L,
questionsCount = 10
)
val loadedStateWithDifferentSelection = CreateQuizState.Loaded(
- specializations = listOf(defaultVoSpecial, defaultVoSpecialWithImage),
+ specializations = persistentListOf(defaultVoSpecial, defaultVoSpecialWithImage),
selectedSpecializationId = 1L,
questionsCount = 5
)
From 03170408fb359a17bea013919ef7f05fa7518864 Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:16:36 +0300
Subject: [PATCH 05/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-58:=20=D0=9F=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20?=
=?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF=20InterviewQuizScreen.=20=D0=9F=D0=BE?=
=?UTF-8?q?=D0=BB=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D1=82=D0=BA?=
=?UTF-8?q?=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BE=D0=B2=20(#97)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-55 добавлен экран тренировки и класс состояний тренировки
* ANDR-55: добавление иконок
* ANDR-55: верстка экрана, добавление в QuizState enum class, создание экрана загрузки
* ANDR-55: добавление строк для экрана тренажера
* ANDR-55: добавление LoadingScreen
* ANDR-55: добавление Command, Event, ScreenMapper, State
* ANDR-55: обновление string ресурсов
* ANDR-55: создание и написание ViewModel
* ANDR-55: окончательная верстка. Создание динамического превью. Добавление дополнительно состояния для статического превью
* ANDR-55: увеличение длины ответа для данных превью
* ANDR-55: рефакторинг. Кнопка Проверить результат м.б. неактивной
* ANDR-55: увеличение длины ответа для данных превью
* ANDR-55: удаление превью из LoadingScreen
* ANDR-58: из Command и Event наследники заменены на ToDo
* ANDR-58: рефакторинг QuizScreen и QuizScreenLoading
рефакторинг ViewModel. Перенесена логика из ScreenState в ScreenMapper. Из тела ScreenState убрана логика, добавлены новые поля
* ANDR-58: подключение immutable библиотеки
* ANDR-58: В стейте Loaded изменение коллекций на Persistent
* ANDR-58: изменение под Persistent коллекции стейта
* ANDR-58: добавление static preview
* ANDR-58: убраны лишние изменения
---
.../interview-trainer/impl/build.gradle.kts | 1 +
.../presentation/InterviewQuizCommand.kt | 6 +
.../presentation/InterviewQuizEvent.kt | 6 +
.../presentation/InterviewQuizScreenMapper.kt | 39 ++
.../presentation/InterviewQuizState.kt | 37 ++
.../presentation/InterviewQuizViewModel.kt | 91 +++
.../interviewQuiz/ui/InterviewQuizScreen.kt | 549 ++++++++++++++++++
.../ui/InterviewQuizScreenLoading.kt | 47 ++
.../res/drawable-nodpi/arrow_left_alt.xml | 10 +
.../res/drawable-nodpi/arrow_right_alt.xml | 10 +
.../drawable-nodpi/favorite_outlined_icon.xml | 10 +
.../res/drawable-nodpi/thumbs_down_icon.xml | 9 +
.../res/drawable-nodpi/thumbs_up_icon.xml | 9 +
.../src/main/res/drawable/ellipse_icon.xml | 9 +
.../res/drawable/favorite_filled_icon.xml | 13 +
.../impl/src/main/res/values/strings.xml | 8 +
16 files changed, 854 insertions(+)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml
create mode 100644 feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml
diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts
index 4ab71943..b6dbb5eb 100644
--- a/feature/interview-trainer/impl/build.gradle.kts
+++ b/feature/interview-trainer/impl/build.gradle.kts
@@ -52,6 +52,7 @@ dependencies {
// Блики
implementation(libs.compose.shimmer)
+ implementation(libs.immutable.collections)
// Неизменяемые коллекции
implementation(libs.immutable.collections)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
new file mode 100644
index 00000000..2fca28c0
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+sealed interface InterviewQuizCommand {
+
+ data object ToDo : InterviewQuizCommand
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
new file mode 100644
index 00000000..63fdf560
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+sealed interface InterviewQuizEvent {
+
+ data object ToDo : InterviewQuizEvent
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
new file mode 100644
index 00000000..8681cff5
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
@@ -0,0 +1,39 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.PersistentMap
+
+class InterviewQuizScreenMapper {
+
+ fun getScreenState(
+ questions: PersistentList,
+ questionIndex: Int,
+ isAnswerVisible: Boolean,
+ answers: PersistentMap,
+ selectedAnswer: InterviewQuizState.Loaded.QuizAnswer
+ ): InterviewQuizState {
+ val canGoNext = answers.containsKey(questions[questionIndex].id) &&
+ questionIndex != questions.lastIndex
+
+ val canGoPrev = questionIndex > 0
+
+ val question = questions[questionIndex]
+
+ val questionsCount = questions.size
+
+ val isLastQuestion = questionIndex != questions.lastIndex
+
+ return InterviewQuizState.Loaded(
+ questions = questions,
+ questionsCount = questionsCount,
+ questionIndex = questionIndex,
+ question = question,
+ isAnswerVisible = isAnswerVisible,
+ answers = answers,
+ canGoNext = canGoNext,
+ canGoPrev = canGoPrev,
+ selectedAnswer = selectedAnswer,
+ isLastQuestion = isLastQuestion
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
new file mode 100644
index 00000000..4e2b89f2
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
@@ -0,0 +1,37 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.PersistentMap
+
+sealed interface InterviewQuizState {
+
+ /** Изначальное состояние */
+ data object Loading : InterviewQuizState
+
+ @Immutable
+ data class Loaded(
+ val questions: PersistentList,
+ val questionsCount: Int,
+ val questionIndex: Int,
+ val question: VoQuestion,
+ val isAnswerVisible: Boolean,
+ val answers: PersistentMap,
+ val canGoPrev: Boolean,
+ val canGoNext: Boolean,
+ val selectedAnswer: QuizAnswer,
+ val isLastQuestion: Boolean
+ ) : InterviewQuizState {
+
+ enum class QuizAnswer { KNOWN, UNKNOWN, NONE }
+
+ @Immutable
+ data class VoQuestion(
+ val id: Long,
+ val title: String,
+ val shortAnswer: String
+ )
+ }
+
+ data class Error(val throwable: Throwable) : InterviewQuizState
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
new file mode 100644
index 00000000..f23c30ca
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
@@ -0,0 +1,91 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.PersistentMap
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import ru.yeahub.core_utils.BaseViewModel
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.QuizAnswer
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.VoQuestion
+
+open class InterviewQuizViewModel(
+ private val screenMapper: InterviewQuizScreenMapper
+) : BaseViewModel() {
+
+ // Вопросы для превью. Временно
+ private val previewQuestions by lazy {
+ previewQuestions()
+ }
+
+ private val userInputState = MutableStateFlow(
+ UserInput(
+ isAnswerVisible = false,
+ answers = persistentMapOf(),
+ selectedAnswer = QuizAnswer.NONE
+ )
+ )
+
+ val screenState = userInputState
+ .map { userInput ->
+ screenMapper.getScreenState(
+ questions = previewQuestions,
+ questionIndex = FIRST_QUESTION_INDEX,
+ isAnswerVisible = userInput.isAnswerVisible,
+ answers = userInput.answers,
+ selectedAnswer = userInput.selectedAnswer
+ )
+ }.stateIn(
+ scope = viewModelScopeSafe,
+ started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
+ initialValue = InterviewQuizState.Loading
+ )
+
+ private val _commands = MutableSharedFlow()
+ val commands = _commands.asSharedFlow()
+
+ fun onEvent(event: InterviewQuizEvent) {
+ when (event) {
+ InterviewQuizEvent.ToDo -> { /* TODO */ }
+ }
+ }
+
+ /** Создание списка вопросов для тестирования превью */
+ @Suppress("MagicNumber")
+ private fun previewQuestions(): PersistentList {
+ val shortAnswer = "Виртуальный DOM (VDOM) — это легковесное " +
+ "представление реального DOM в памяти, которое используется в " +
+ "JavaScript-библиотеках, таких как React и Vue, " +
+ "для повышения производительности веб-приложений."
+
+ val base = VoQuestion(
+ id = 0,
+ title = "Что такое Virtual DOM, и как он работает?",
+ shortAnswer = shortAnswer
+ )
+ val questions = mutableListOf()
+ repeat(10) { index ->
+ questions.add(base.copy(id = index.toLong()))
+ }
+
+ return questions.toPersistentList()
+ }
+
+ private data class UserInput(
+ val isAnswerVisible: Boolean,
+ val answers: PersistentMap,
+ val selectedAnswer: QuizAnswer
+ )
+
+ companion object {
+
+ private const val TIME_TO_CLEAN_UP_RESOURCES = 5000L
+
+ private const val FIRST_QUESTION_INDEX = 0
+ }
+}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
new file mode 100644
index 00000000..230a248c
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
@@ -0,0 +1,549 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import ru.yeahub.core_ui.component.ErrorScreen
+import ru.yeahub.core_ui.component.PrimaryButton
+import ru.yeahub.core_ui.component.SecondaryButton
+import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
+import ru.yeahub.core_ui.component.YeahubButtonDefaults
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.Theme
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizEvent
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizScreenMapper
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizViewModel
+
+private val FIGMA_MEDIUM_PADDING = 16.dp
+private val FIGMA_LOW_PADDING = 8.dp
+private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
+
+private val FIGMA_CARD_ELEVATION = 4.dp
+private val FIGMA_RADIUS = 12.dp
+
+@Composable
+private fun ScreenUI(
+ headerText: TextOrResource,
+ state: InterviewQuizState,
+ onEvent: (InterviewQuizEvent) -> Unit
+) {
+ Scaffold(
+ containerColor = Theme.colors.black10,
+ topBar = {
+ TopAppBarWithBottomBorder(
+ title = headerText,
+ onBackClick = { TODO("onBackClick don't implemented") }
+ )
+ }
+ ) { paddingValues ->
+ Box(Modifier.padding(paddingValues)) {
+ when (state) {
+ is InterviewQuizState.Loaded -> BaseQuizScreen(
+ state = state,
+ onPreviousClick = { /* TODO */ },
+ onNextClick = { /* TODO */ },
+ onUnknownClick = { /* TODO */ },
+ onKnownClick = { /* TODO */ },
+ onShowAnswerClick = { /* TODO */ },
+ onResultClick = { /* TODO */ }
+ )
+
+ is InterviewQuizState.Error -> ErrorScreen(
+ error = state.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = { TODO() }
+ )
+
+ InterviewQuizState.Loading -> InterviewQuizLoading()
+ }
+ }
+ }
+}
+
+@Composable
+private fun BaseQuizScreen(
+ state: InterviewQuizState.Loaded,
+ onPreviousClick: () -> Unit,
+ onNextClick: () -> Unit,
+ onUnknownClick: () -> Unit,
+ onKnownClick: () -> Unit,
+ onShowAnswerClick: () -> Unit,
+ onResultClick: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(
+ horizontal = FIGMA_MEDIUM_PADDING,
+ vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING
+ )
+ ) {
+ QuizProgress(
+ current = state.questionIndex + 1,
+ total = state.questionsCount,
+ )
+ Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING))
+ QuestionCard(
+ state = state,
+ onPreviousClick = onPreviousClick,
+ onNextClick = onNextClick,
+ onUnknownClick = onUnknownClick,
+ onKnownClick = onKnownClick,
+ onShowAnswerClick = onShowAnswerClick,
+ onResultClick = onResultClick,
+ )
+ }
+}
+
+@Composable
+private fun QuizProgress(
+ current: Int,
+ total: Int,
+ modifier: Modifier = Modifier
+) {
+ val progress = (current.toFloat() / total)
+
+ DefaultCard(modifier) {
+ Column(modifier = Modifier.padding(FIGMA_MEDIUM_PADDING)) {
+ LinearProgressIndicator(
+ progress = { progress },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .clip(RoundedCornerShape(24)),
+ color = Theme.colors.purple700,
+ trackColor = Theme.colors.purple300,
+ gapSize = (-8).dp,
+ strokeCap = StrokeCap.Round,
+ drawStopIndicator = {}
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = TextOrResource.Text("$current из $total").text,
+ color = Theme.colors.black500,
+ style = Theme.typography.body2Accent,
+ modifier = Modifier.align(Alignment.End)
+ )
+ }
+ }
+}
+
+@Composable
+private fun QuestionCard(
+ state: InterviewQuizState.Loaded,
+ onPreviousClick: () -> Unit,
+ onNextClick: () -> Unit,
+ onUnknownClick: () -> Unit,
+ onKnownClick: () -> Unit,
+ onShowAnswerClick: () -> Unit,
+ onResultClick: () -> Unit
+) {
+ // TODO: нет фичи профиля
+ var isFavorite by rememberSaveable { mutableStateOf(false) }
+
+ val favoriteIcon: Painter =
+ painterResource(
+ if (isFavorite) {
+ R.drawable.favorite_filled_icon
+ } else {
+ R.drawable.favorite_outlined_icon
+ }
+ )
+
+ DefaultCard {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(FIGMA_MEDIUM_PADDING)
+ ) {
+ Row(Modifier.fillMaxWidth()) {
+ NavigationButton(
+ text = TextOrResource.Resource(R.string.quiz_btn_prev),
+ enabled = state.canGoPrev,
+ onClick = onPreviousClick,
+ leadingIcon = painterResource(R.drawable.arrow_left_alt)
+ )
+ Spacer(Modifier.weight(1f))
+ NavigationButton(
+ text = TextOrResource.Resource(R.string.quiz_btn_next),
+ enabled = state.canGoNext,
+ onClick = onNextClick,
+ trailingIcon = painterResource(R.drawable.arrow_right_alt)
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = FIGMA_MEDIUM_PADDING),
+ verticalAlignment = Alignment.Top
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ellipse_icon),
+ contentDescription = null,
+ modifier = Modifier.padding(top = 4.dp),
+ tint = Theme.colors.purple800
+ )
+ Text(
+ text = state.question.title,
+ modifier = Modifier
+ .padding(start = FIGMA_LOW_PADDING, end = 12.dp)
+ .weight(1f),
+ style = Theme.typography.body3Strong
+ )
+ FilledIconButton(
+ onClick = { isFavorite = !isFavorite },
+ modifier = Modifier.size(48.dp),
+ shape = RoundedCornerShape(FIGMA_RADIUS),
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = Theme.colors.black10
+ )
+ ) {
+ Icon(
+ painter = favoriteIcon,
+ contentDescription = null,
+ modifier = Modifier.padding(12.dp),
+ tint = if (isFavorite) {
+ Theme.colors.red700
+ } else {
+ Theme.colors.black600
+ }
+ )
+ }
+ }
+ Text(
+ text = if (state.isAnswerVisible) {
+ stringResource(R.string.quiz_collapse_answer)
+ } else {
+ stringResource(R.string.quiz_show_answer)
+ },
+ modifier = Modifier
+ .padding(top = FIGMA_MEDIUM_PADDING, start = 12.dp)
+ .clickable(onClick = onShowAnswerClick),
+ style = Theme.typography.body2,
+ color = Theme.colors.purple700
+ )
+ if (state.isAnswerVisible) {
+ Text(
+ text = state.question.shortAnswer,
+ modifier = Modifier.padding(top = 12.dp),
+ style = Theme.typography.body3
+ )
+ }
+ Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING))
+
+ Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) {
+ QuizAnswerButton(
+ painter = painterResource(R.drawable.thumbs_down_icon),
+ text = TextOrResource.Resource(R.string.quiz_answer_unknown),
+ onClick = onUnknownClick,
+ isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN
+ )
+ Spacer(Modifier.weight(1f))
+ QuizAnswerButton(
+ painter = painterResource(R.drawable.thumbs_up_icon),
+ text = TextOrResource.Resource(R.string.quiz_answer_known),
+ onClick = onKnownClick,
+ isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ )
+ }
+ HorizontalDivider(
+ modifier = Modifier.padding(bottom = FIGMA_MEDIUM_PADDING),
+ color = Theme.colors.black100
+ )
+ if (state.isLastQuestion) {
+ PrimaryButton(
+ onClick = onResultClick,
+ modifier = Modifier.height(48.dp),
+ enabled = state.selectedAnswer != InterviewQuizState.Loaded.QuizAnswer.NONE,
+ colors = YeahubButtonDefaults.primaryButtonColors(
+ disabledContentColor = Theme.colors.black100
+ )
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.quiz_check_result),
+ style = Theme.typography.body3Strong,
+ color = Theme.colors.white900
+ )
+ }
+ }
+ } else {
+ SecondaryButton(
+ onClick = {},
+ modifier = Modifier
+ .width(170.dp)
+ .height(48.dp)
+ .align(Alignment.End),
+ colors = YeahubButtonDefaults.secondaryButtonColors(
+ containerColor = Theme.colors.red100,
+ contentColor = Theme.colors.red700
+ )
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.quiz_btn_complete),
+ style = Theme.typography.body3Strong
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DefaultCard(
+ modifier: Modifier = Modifier,
+ content: @Composable (ColumnScope.() -> Unit)
+) {
+ Card(
+ modifier = modifier,
+ colors = CardDefaults.cardColors(containerColor = Theme.colors.white900),
+ shape = RoundedCornerShape(FIGMA_RADIUS),
+ elevation = CardDefaults.cardElevation(FIGMA_CARD_ELEVATION),
+ content = content
+ )
+}
+
+@Composable
+private fun NavigationButton(
+ text: TextOrResource,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ leadingIcon: Painter? = null,
+ trailingIcon: Painter? = null,
+ contentPadding: PaddingValues = PaddingValues()
+) {
+ val context = LocalContext.current
+ val color = if (enabled) Theme.colors.purple700 else Theme.colors.purple300
+
+ TextButton(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ contentPadding = contentPadding
+ ) {
+ if (leadingIcon != null) {
+ Icon(
+ painter = leadingIcon,
+ contentDescription = null,
+ tint = color
+ )
+ Spacer(Modifier.width(FIGMA_LOW_PADDING))
+ }
+ Text(
+ text = when (text) {
+ is TextOrResource.Resource -> text.getString(context)
+ is TextOrResource.Text -> text.text
+ },
+ color = color,
+ style = Theme.typography.body3Strong
+ )
+ if (trailingIcon != null) {
+ Spacer(Modifier.width(FIGMA_LOW_PADDING))
+ Icon(
+ painter = trailingIcon,
+ contentDescription = null,
+ tint = color
+ )
+ }
+ }
+}
+
+@Composable
+private fun QuizAnswerButton(
+ painter: Painter,
+ text: TextOrResource,
+ onClick: () -> Unit,
+ isSelected: Boolean
+) {
+ val context = LocalContext.current
+
+ val contentColor = if (isSelected) {
+ Theme.colors.purple700
+ } else {
+ Theme.colors.black700
+ }
+
+ Button(
+ onClick = onClick,
+ modifier = Modifier
+ .width(120.dp)
+ .height(48.dp),
+ shape = RoundedCornerShape(FIGMA_RADIUS),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Theme.colors.black10,
+ contentColor = contentColor
+ ),
+ contentPadding = PaddingValues(
+ horizontal = 12.dp,
+ vertical = FIGMA_LOW_PADDING
+ )
+ ) {
+ Icon(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier.padding(end = FIGMA_LOW_PADDING)
+ )
+ Text(
+ text = when (text) {
+ is TextOrResource.Text -> text.text
+ is TextOrResource.Resource -> text.getString(context)
+ },
+ style = Theme.typography.body2
+ )
+ }
+}
+
+private val shortAnswerForPreview = "Виртуальный DOM (VDOM) — это легковесное " +
+ "представление реального DOM в памяти, которое используется в " +
+ "JavaScript-библиотеках, таких как React и Vue, " +
+ "для повышения производительности веб-приложений."
+private val questionForPreview = InterviewQuizState.Loaded.VoQuestion(
+ id = 0,
+ title = "Что такое Virtual DOM, и как он работает?",
+ shortAnswer = shortAnswerForPreview
+)
+private val questions = persistentListOf(
+ questionForPreview,
+ questionForPreview.copy(id = 1),
+ questionForPreview.copy(id = 2),
+ questionForPreview.copy(id = 3)
+)
+private val answers = persistentMapOf(
+ 1.toLong() to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ 2.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ 3.toLong() to InterviewQuizState.Loaded.QuizAnswer.KNOWN
+)
+
+class QuizScreenStateParamProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(
+ InterviewQuizState.Loaded(
+ questions = questions,
+ questionsCount = questions.size,
+ questionIndex = 0,
+ question = questions[0],
+ isAnswerVisible = false,
+ answers = answers,
+ canGoPrev = false,
+ canGoNext = false,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.NONE,
+ isLastQuestion = false
+ ),
+ InterviewQuizState.Loaded(
+ questions = questions,
+ questionsCount = questions.size,
+ questionIndex = 2,
+ question = questions[2],
+ isAnswerVisible = true,
+ answers = answers,
+ canGoPrev = true,
+ canGoNext = true,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ isLastQuestion = false
+ ),
+ InterviewQuizState.Loaded(
+ questions = questions,
+ questionsCount = questions.size,
+ questionIndex = 3,
+ question = questions[3],
+ isAnswerVisible = false,
+ answers = answers,
+ canGoPrev = true,
+ canGoNext = false,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ isLastQuestion = true
+ ),
+ InterviewQuizState.Loading,
+ InterviewQuizState.Error(
+ Throwable("Не удалось загрузить данные")
+ )
+ )
+}
+
+@StaticPreview
+@Composable
+fun InterviewQuizScreen(
+ @PreviewParameter(QuizScreenStateParamProvider::class)
+ state: InterviewQuizState
+) {
+ ScreenUI(
+ headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ state = state,
+ onEvent = {}
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DynamicPreviewUI() {
+ val mockViewModel = viewModel {
+ InterviewQuizViewModel(InterviewQuizScreenMapper())
+ }
+
+ val state by mockViewModel.screenState.collectAsState()
+
+ ScreenUI(
+ headerText = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ state = state,
+ onEvent = mockViewModel::onEvent
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt
new file mode 100644
index 00000000..7fd7bab7
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreenLoading.kt
@@ -0,0 +1,47 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.valentinilk.shimmer.shimmer
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.Theme
+
+@Composable
+fun InterviewQuizLoading() {
+ Column(Modifier.padding(horizontal = 16.dp).fillMaxSize()) {
+ PlaceHolderBlock(
+ Modifier.padding(vertical = 24.dp).fillMaxWidth().height(65.dp)
+ )
+ PlaceHolderBlock(Modifier.fillMaxWidth().height(320.dp))
+ }
+}
+
+@Composable
+private fun PlaceHolderBlock(modifier: Modifier = Modifier) {
+ Card(
+ modifier = modifier.shimmer(),
+ colors = CardDefaults.cardColors(containerColor = Theme.colors.white900),
+ shape = RoundedCornerShape(12.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Box(Modifier.fillMaxSize().background(Color.LightGray))
+ }
+}
+
+@StaticPreview
+@Composable
+fun InterviewQuizLoadingPreview() {
+ InterviewQuizLoading()
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml
new file mode 100644
index 00000000..cbc3319d
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_left_alt.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml
new file mode 100644
index 00000000..8fe08e66
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/arrow_right_alt.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml
new file mode 100644
index 00000000..7d6e597c
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/favorite_outlined_icon.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml
new file mode 100644
index 00000000..dc4be875
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml
new file mode 100644
index 00000000..0b07152f
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml
new file mode 100644
index 00000000..c0481dc1
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable/ellipse_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml
new file mode 100644
index 00000000..7cba0263
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/res/drawable/favorite_filled_icon.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index c72f7ccf..26739f27 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -10,4 +10,12 @@
Что‑то пошло не так
Назад
Не удалось загрузить данные
+ Проверить результаты
+ Свернуть ответ
+ Показать ответ
+ Не знаю
+ Знаю
+ Завершить
+ Назад
+ Далее
\ No newline at end of file
From 3785e6b70d64ea65a99661fd63a2dc62fe6a4eca Mon Sep 17 00:00:00 2001
From: PanMobile
Date: Fri, 20 Feb 2026 19:51:43 +0300
Subject: [PATCH 06/28] =?UTF-8?q?ANDR-71:=20=D0=9E=D0=B1=D1=80=D0=B0=D0=B1?=
=?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?=
=?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?=
=?UTF-8?q?=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../presentation/CreateQuizViewModel.kt | 31 ++++++++++++-------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
index f0f4ccd6..4ab73f5c 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -1,10 +1,12 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -24,21 +26,26 @@ open class CreateQuizViewModel(
questionsCount = MIN_QUESTIONS_COUNT
)
)
+ val request = SpecializationsRequest(page = 1, limit = 99)
+ val specsDef = viewModelScopeSafe.async {
+ getSpecializationsListUseCase(request).data
+ }
- val screenState = userInputState
- .map { userInput ->
- val request = SpecializationsRequest(page = 1, limit = 99)
+ val screenState = userInputState.map { userInput ->
+ val specializations = specsDef.await()
- screenMapper.getScreenState(
- specializations = getSpecializationsListUseCase(request).data,
- selectedSpecializationId = userInput.selectedSpecializationId,
- questionsCount = userInput.questionsCount
- )
- }.stateIn(
- scope = viewModelScopeSafe,
- started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
- initialValue = CreateQuizState.Loading
+ screenMapper.getScreenState(
+ specializations = specializations,
+ selectedSpecializationId = userInput.selectedSpecializationId,
+ questionsCount = userInput.questionsCount,
)
+ }.catch { throwable ->
+ emit(screenMapper.getScreenState(throwable))
+ }.stateIn(
+ scope = viewModelScopeSafe,
+ started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
+ initialValue = CreateQuizState.Loading
+ )
private val _commands = MutableSharedFlow()
val commands: SharedFlow = _commands
From 398ee9a35640b5151f9fd1afc920851039e7f3c3 Mon Sep 17 00:00:00 2001
From: PanMobile
Date: Fri, 20 Feb 2026 20:24:30 +0300
Subject: [PATCH 07/28] =?UTF-8?q?ANDR-71:=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?=
=?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81?=
=?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20contentDescription?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
feature/interview-trainer/impl/src/main/res/values/strings.xml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index 26739f27..e2beb915 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -5,6 +5,8 @@
Выбор специализации
Количество вопросов
Начать
+ Уменьшить количество вопросов
+ Увеличить количество вопросов
УПС!
Что‑то пошло не так
From 13668edec93a4df23e683796b22432d2eadfe4a1 Mon Sep 17 00:00:00 2001
From: PanMobile
Date: Fri, 20 Feb 2026 20:25:32 +0300
Subject: [PATCH 08/28] =?UTF-8?q?ANDR-71:=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D1=82=D0=BA?=
=?UTF-8?q?=D0=B8=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D0=B7=D0=B0?=
=?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8;=20=D0=B4=D0=BE=D0=B1?=
=?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80?=
=?UTF-8?q?=D0=B5=D0=B4=D0=B0=D0=B2=D0=B0=D0=B5=D0=BC=D0=BE=D0=B3=D0=BE=20?=
=?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B0=20=D0=BE?=
=?UTF-8?q?=D1=82=D1=81=D1=82=D1=83=D0=BF=D0=BE=D0=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../createQuiz/ui/CreateQuizScreenLoading.kt | 49 +++++++++----------
1 file changed, 24 insertions(+), 25 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
index 29138a7f..483c5a75 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -32,36 +33,28 @@ import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
import ru.yeahub.interview_trainer.impl.R
-private val FIGMA_HORIZONTAL_PADDING = 16.dp
-private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
-private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
-
-@StaticPreview
@Composable
fun CreateQuizLoading(
+ paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
- Column(
- modifier = modifier
- .padding(horizontal = FIGMA_HORIZONTAL_PADDING)
- ) {
- Text(
- modifier = Modifier
- .padding(vertical = 24.dp),
- style = LocalAppTypography.current.head5,
- text = stringResource(R.string.create_quiz_screen_main_title),
- color = colors.black900
- )
-
- PlaceHolderBlock()
-
- Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+ Box(modifier = modifier.padding(paddingValues)) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = 24.dp),
+ style = LocalAppTypography.current.head5,
+ text = stringResource(R.string.create_quiz_screen_main_title),
+ color = colors.black900
+ )
- PlaceHolderBlock()
+ PlaceHolderBlock()
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.weight(1f))
- DisabledStartQuizButton()
+ DisabledStartQuizButton()
+ }
}
}
@@ -88,7 +81,7 @@ private fun PlaceHolderBlock(
Box(
modifier = Modifier
.fillMaxWidth()
- .height(148.dp)
+ .height(640.dp)
.background(Color.LightGray, shape = RoundedCornerShape(4.dp))
)
}
@@ -100,7 +93,7 @@ private fun DisabledStartQuizButton(
) {
SecondaryButton(
modifier = modifier
- .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
+ .padding(vertical = 24.dp)
.height(48.dp)
.fillMaxWidth(),
enabled = false,
@@ -128,4 +121,10 @@ private fun DisabledStartQuizButton(
)
}
}
+}
+
+@StaticPreview
+@Composable
+internal fun StaticPreviewCreateQuizLoading() {
+ CreateQuizLoading(paddingValues = PaddingValues(0.dp))
}
\ No newline at end of file
From ea3151b5c4765b8312741251568eee9ca41763ea Mon Sep 17 00:00:00 2001
From: PanMobile
Date: Fri, 20 Feb 2026 20:28:24 +0300
Subject: [PATCH 09/28] =?UTF-8?q?ANDR-71:=20=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB?=
=?UTF-8?q?=D1=8C=D1=88=D0=BE=D0=B9=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?=
=?UTF-8?q?=D0=BE=D1=80=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=B3?=
=?UTF-8?q?=D0=BE=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20(Box=20=D0=B2?=
=?UTF-8?q?=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=D0=BD=D1=83=D1=82?=
=?UTF-8?q?=D1=80=D1=8C=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BE=D0=B2,=20?=
=?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=BD=D0=B5?=
=?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D0=B5=D1=82=D1=81=D1=8F?=
=?UTF-8?q?,=20=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=B5=D1=82=D1=81?=
=?UTF-8?q?=D1=8F=20=D0=B2=D0=BD=D1=83=D1=82=D1=80=D0=B8=20=D0=BA=D0=B0?=
=?UTF-8?q?=D0=B6=D0=B4=D0=BE=D0=B9=20@Composable,=20=D1=83=D0=B1=D1=80?=
=?UTF-8?q?=D0=B0=D0=BD=20=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4=20con?=
=?UTF-8?q?tentDescription)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/createQuiz/ui/CreateQuizScreen.kt | 171 +++++++++---------
1 file changed, 85 insertions(+), 86 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index 9a93809d..eafa5206 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -1,6 +1,5 @@
package ru.yeahub.interview_trainer.impl.createQuiz.ui
-import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -31,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -67,8 +67,6 @@ import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreen
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
-private val FIGMA_HORIZONTAL_PADDING = 16.dp
-private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
@Composable
@@ -82,7 +80,7 @@ fun CreateQuizScreen(
HandleCommand(
commandFlow = viewModel.commands,
- onResult = { result -> onResult(result) }
+ onResult = onResult
)
ScreenUI(
@@ -107,49 +105,42 @@ private fun ScreenUI(
)
}
) { paddingValues ->
- Box(
- modifier = Modifier
- .padding(paddingValues)
- .verticalScroll(rememberScrollState())
- ) {
- when (state) {
- CreateQuizState.Loading -> CreateQuizLoading()
-
- is CreateQuizState.Error -> {
- ErrorScreen(
- error = state.throwable.localizedMessage,
- errorText = TextOrResource.Resource(R.string.error_screen_text),
- titleText = TextOrResource.Resource(R.string.title_error_screen_text),
- backText = TextOrResource.Resource(R.string.back_error_screen_text),
- unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
- onBack = { onEvent(CreateQuizEvent.OnBackClick) }
- )
- }
-
- is CreateQuizState.Loaded -> BaseCreateQuizScreen(
- specializations = state.specializations,
- selectedSpecializationId = state.selectedSpecializationId,
- questionsCount = state.questionsCount,
- onSpecializationClick = { id ->
- onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
- },
- onPlusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
- },
- onMinusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
- },
- onStartQuizClick = { specializationId: Long, questionsCount: Int ->
- onEvent(
- CreateQuizEvent.OnStartInterviewQuizClick(
- specializationId = specializationId,
- questionCount = questionsCount,
- )
+ when (state) {
+ CreateQuizState.Loading -> CreateQuizLoading(paddingValues = paddingValues)
+
+ is CreateQuizState.Error -> ErrorScreen(
+ error = state.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = { onEvent(CreateQuizEvent.OnBackClick) }
+ )
+
+ is CreateQuizState.Loaded -> BaseCreateQuizScreen(
+ specializations = state.specializations,
+ selectedSpecializationId = state.selectedSpecializationId,
+ questionsCount = state.questionsCount,
+ onSpecializationClick = { id ->
+ onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
+ },
+ onPlusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
+ },
+ onMinusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
+ },
+ onStartQuizClick = { specializationId: Long, questionsCount: Int ->
+ onEvent(
+ CreateQuizEvent.OnStartInterviewQuizClick(
+ specializationId = specializationId,
+ questionCount = questionsCount,
)
- },
- titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title)
- )
- }
+ )
+ },
+ titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title),
+ paddingValues = paddingValues
+ )
}
}
}
@@ -182,46 +173,50 @@ private fun BaseCreateQuizScreen(
onMinusQuestionCountClick: (count: Int) -> Unit,
onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
titleText: TextOrResource,
+ paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
- Column(
+ Box(
modifier = modifier
- .padding(horizontal = FIGMA_HORIZONTAL_PADDING),
+ .padding(paddingValues = paddingValues)
+ .verticalScroll(rememberScrollState())
) {
- Text(
- modifier = Modifier
- .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
- text = titleText.getString(context),
- style = LocalAppTypography.current.head5,
- )
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
+ text = titleText.getString(context),
+ style = LocalAppTypography.current.head5,
+ )
- ChooseSpecializationBlock(
- context = context,
- specializations = specializations,
- selectedSpecializationId = selectedSpecializationId,
- onSpecializationClick = onSpecializationClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
- )
+ ChooseSpecializationBlock(
+ specializations = specializations,
+ selectedSpecializationId = selectedSpecializationId,
+ onSpecializationClick = onSpecializationClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
+ )
- Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+ Spacer(modifier = Modifier.height(16.dp))
- ChooseQuestionsCountBlock(
- context = context,
- questionsCount = questionsCount,
- onPlusQuestionCountClick = onPlusQuestionCountClick,
- onMinusQuestionCountClick = onMinusQuestionCountClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
- )
+ ChooseQuestionsCountBlock(
+ questionsCount = questionsCount,
+ onPlusQuestionCountClick = onPlusQuestionCountClick,
+ onMinusQuestionCountClick = onMinusQuestionCountClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
+ )
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.weight(1f))
- StartQuizButton(
- specializationId = selectedSpecializationId,
- questionsCount = questionsCount,
- onStartQuizClick = onStartQuizClick,
- )
+ StartQuizButton(
+ specializationId = selectedSpecializationId,
+ questionsCount = questionsCount,
+ onStartQuizClick = onStartQuizClick,
+ )
+ }
}
}
@@ -229,11 +224,12 @@ private fun BaseCreateQuizScreen(
private fun ChooseSpecializationBlock(
specializations: ImmutableList,
selectedSpecializationId: Long,
- context: Context,
onSpecializationClick: (Long) -> Unit,
titleText: TextOrResource,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
+
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
@@ -269,13 +265,14 @@ private fun ChooseSpecializationBlock(
@Composable
private fun ChooseQuestionsCountBlock(
- context: Context,
questionsCount: Int,
onPlusQuestionCountClick: (count: Int) -> Unit,
onMinusQuestionCountClick: (count: Int) -> Unit,
titleText: TextOrResource,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
+
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
@@ -316,7 +313,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.minus_icon),
- contentDescription = "Decrease questions count",
+ contentDescription = stringResource(R.string.decrease_question_count_content_description),
tint = colors.black600
)
}
@@ -334,7 +331,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.plus_icon),
- contentDescription = "Increase questions count",
+ contentDescription = stringResource(R.string.increase_question_count_content_description),
tint = colors.black600,
)
}
@@ -349,6 +346,8 @@ private fun StartQuizButton(
onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
+
PrimaryButton(
modifier = modifier
.padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
@@ -362,7 +361,7 @@ private fun StartQuizButton(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "Начать",
+ text = stringResource(R.string.create_quiz_start_quiz_button_text),
style = LocalAppTypography.current.body3Strong,
textAlign = TextAlign.Center,
color = colors.white900
@@ -380,7 +379,7 @@ private fun StartQuizButton(
}
}
-val specializations = persistentListOf(
+private val testSpecializations = persistentListOf(
CreateQuizState.Loaded.VoSpecialization(
id = 11,
title = "Frontend"
@@ -406,7 +405,7 @@ val specializations = persistentListOf(
title = "iOS Dev"
),
CreateQuizState.Loaded.VoSpecialization(
- id = 21,
+ id = 27,
title = "Android Dev"
),
CreateQuizState.Loaded.VoSpecialization(
@@ -418,7 +417,7 @@ val specializations = persistentListOf(
class CreateQuizScreenStateParamProvider : PreviewParameterProvider {
override val values: Sequence = sequenceOf(
CreateQuizState.Loaded(
- specializations = specializations,
+ specializations = testSpecializations,
selectedSpecializationId = 11,
questionsCount = 1
),
@@ -431,7 +430,7 @@ class CreateQuizScreenStateParamProvider : PreviewParameterProvider
+internal fun DynamicPreviewUI() {
+ val mockDomainList = testSpecializations.map { voSpec ->
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
From 1b7ba916a19b079cd319a782b7f87d12c42130eb Mon Sep 17 00:00:00 2001
From: PanMobile
Date: Fri, 20 Feb 2026 20:31:05 +0300
Subject: [PATCH 10/28] =?UTF-8?q?Revert=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8?=
=?UTF-8?q?=D1=82=D0=BE=D0=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit 3785e6b70d64ea65a99661fd63a2dc62fe6a4eca, 398ee9a35640b5151f9fd1afc920851039e7f3c3, 13668edec93a4df23e683796b22432d2eadfe4a1, ea3151b5c4765b8312741251568eee9ca41763ea.
---
.../presentation/CreateQuizViewModel.kt | 31 ++--
.../impl/createQuiz/ui/CreateQuizScreen.kt | 171 +++++++++---------
.../createQuiz/ui/CreateQuizScreenLoading.kt | 49 ++---
.../impl/src/main/res/values/strings.xml | 2 -
4 files changed, 123 insertions(+), 130 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
index 4ab73f5c..f0f4ccd6 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -1,12 +1,10 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -26,26 +24,21 @@ open class CreateQuizViewModel(
questionsCount = MIN_QUESTIONS_COUNT
)
)
- val request = SpecializationsRequest(page = 1, limit = 99)
- val specsDef = viewModelScopeSafe.async {
- getSpecializationsListUseCase(request).data
- }
- val screenState = userInputState.map { userInput ->
- val specializations = specsDef.await()
+ val screenState = userInputState
+ .map { userInput ->
+ val request = SpecializationsRequest(page = 1, limit = 99)
- screenMapper.getScreenState(
- specializations = specializations,
- selectedSpecializationId = userInput.selectedSpecializationId,
- questionsCount = userInput.questionsCount,
+ screenMapper.getScreenState(
+ specializations = getSpecializationsListUseCase(request).data,
+ selectedSpecializationId = userInput.selectedSpecializationId,
+ questionsCount = userInput.questionsCount
+ )
+ }.stateIn(
+ scope = viewModelScopeSafe,
+ started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
+ initialValue = CreateQuizState.Loading
)
- }.catch { throwable ->
- emit(screenMapper.getScreenState(throwable))
- }.stateIn(
- scope = viewModelScopeSafe,
- started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
- initialValue = CreateQuizState.Loading
- )
private val _commands = MutableSharedFlow()
val commands: SharedFlow = _commands
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index eafa5206..9a93809d 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -1,5 +1,6 @@
package ru.yeahub.interview_trainer.impl.createQuiz.ui
+import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -30,7 +31,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -67,6 +67,8 @@ import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreen
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
+private val FIGMA_HORIZONTAL_PADDING = 16.dp
+private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
@Composable
@@ -80,7 +82,7 @@ fun CreateQuizScreen(
HandleCommand(
commandFlow = viewModel.commands,
- onResult = onResult
+ onResult = { result -> onResult(result) }
)
ScreenUI(
@@ -105,42 +107,49 @@ private fun ScreenUI(
)
}
) { paddingValues ->
- when (state) {
- CreateQuizState.Loading -> CreateQuizLoading(paddingValues = paddingValues)
-
- is CreateQuizState.Error -> ErrorScreen(
- error = state.throwable.localizedMessage,
- errorText = TextOrResource.Resource(R.string.error_screen_text),
- titleText = TextOrResource.Resource(R.string.title_error_screen_text),
- backText = TextOrResource.Resource(R.string.back_error_screen_text),
- unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
- onBack = { onEvent(CreateQuizEvent.OnBackClick) }
- )
-
- is CreateQuizState.Loaded -> BaseCreateQuizScreen(
- specializations = state.specializations,
- selectedSpecializationId = state.selectedSpecializationId,
- questionsCount = state.questionsCount,
- onSpecializationClick = { id ->
- onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
- },
- onPlusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
- },
- onMinusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
- },
- onStartQuizClick = { specializationId: Long, questionsCount: Int ->
- onEvent(
- CreateQuizEvent.OnStartInterviewQuizClick(
- specializationId = specializationId,
- questionCount = questionsCount,
- )
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .verticalScroll(rememberScrollState())
+ ) {
+ when (state) {
+ CreateQuizState.Loading -> CreateQuizLoading()
+
+ is CreateQuizState.Error -> {
+ ErrorScreen(
+ error = state.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = { onEvent(CreateQuizEvent.OnBackClick) }
)
- },
- titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title),
- paddingValues = paddingValues
- )
+ }
+
+ is CreateQuizState.Loaded -> BaseCreateQuizScreen(
+ specializations = state.specializations,
+ selectedSpecializationId = state.selectedSpecializationId,
+ questionsCount = state.questionsCount,
+ onSpecializationClick = { id ->
+ onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
+ },
+ onPlusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
+ },
+ onMinusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
+ },
+ onStartQuizClick = { specializationId: Long, questionsCount: Int ->
+ onEvent(
+ CreateQuizEvent.OnStartInterviewQuizClick(
+ specializationId = specializationId,
+ questionCount = questionsCount,
+ )
+ )
+ },
+ titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title)
+ )
+ }
}
}
}
@@ -173,50 +182,46 @@ private fun BaseCreateQuizScreen(
onMinusQuestionCountClick: (count: Int) -> Unit,
onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
titleText: TextOrResource,
- paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
- Box(
+ Column(
modifier = modifier
- .padding(paddingValues = paddingValues)
- .verticalScroll(rememberScrollState())
+ .padding(horizontal = FIGMA_HORIZONTAL_PADDING),
) {
- Column(
- modifier = Modifier.padding(horizontal = 16.dp),
- ) {
- Text(
- modifier = Modifier
- .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
- text = titleText.getString(context),
- style = LocalAppTypography.current.head5,
- )
+ Text(
+ modifier = Modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
+ text = titleText.getString(context),
+ style = LocalAppTypography.current.head5,
+ )
- ChooseSpecializationBlock(
- specializations = specializations,
- selectedSpecializationId = selectedSpecializationId,
- onSpecializationClick = onSpecializationClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
- )
+ ChooseSpecializationBlock(
+ context = context,
+ specializations = specializations,
+ selectedSpecializationId = selectedSpecializationId,
+ onSpecializationClick = onSpecializationClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
+ )
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
- ChooseQuestionsCountBlock(
- questionsCount = questionsCount,
- onPlusQuestionCountClick = onPlusQuestionCountClick,
- onMinusQuestionCountClick = onMinusQuestionCountClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
- )
+ ChooseQuestionsCountBlock(
+ context = context,
+ questionsCount = questionsCount,
+ onPlusQuestionCountClick = onPlusQuestionCountClick,
+ onMinusQuestionCountClick = onMinusQuestionCountClick,
+ titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
+ )
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.weight(1f))
- StartQuizButton(
- specializationId = selectedSpecializationId,
- questionsCount = questionsCount,
- onStartQuizClick = onStartQuizClick,
- )
- }
+ StartQuizButton(
+ specializationId = selectedSpecializationId,
+ questionsCount = questionsCount,
+ onStartQuizClick = onStartQuizClick,
+ )
}
}
@@ -224,12 +229,11 @@ private fun BaseCreateQuizScreen(
private fun ChooseSpecializationBlock(
specializations: ImmutableList,
selectedSpecializationId: Long,
+ context: Context,
onSpecializationClick: (Long) -> Unit,
titleText: TextOrResource,
modifier: Modifier = Modifier,
) {
- val context = LocalContext.current
-
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
@@ -265,14 +269,13 @@ private fun ChooseSpecializationBlock(
@Composable
private fun ChooseQuestionsCountBlock(
+ context: Context,
questionsCount: Int,
onPlusQuestionCountClick: (count: Int) -> Unit,
onMinusQuestionCountClick: (count: Int) -> Unit,
titleText: TextOrResource,
modifier: Modifier = Modifier,
) {
- val context = LocalContext.current
-
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
@@ -313,7 +316,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.minus_icon),
- contentDescription = stringResource(R.string.decrease_question_count_content_description),
+ contentDescription = "Decrease questions count",
tint = colors.black600
)
}
@@ -331,7 +334,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.plus_icon),
- contentDescription = stringResource(R.string.increase_question_count_content_description),
+ contentDescription = "Increase questions count",
tint = colors.black600,
)
}
@@ -346,8 +349,6 @@ private fun StartQuizButton(
onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
modifier: Modifier = Modifier,
) {
- val context = LocalContext.current
-
PrimaryButton(
modifier = modifier
.padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
@@ -361,7 +362,7 @@ private fun StartQuizButton(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = stringResource(R.string.create_quiz_start_quiz_button_text),
+ text = "Начать",
style = LocalAppTypography.current.body3Strong,
textAlign = TextAlign.Center,
color = colors.white900
@@ -379,7 +380,7 @@ private fun StartQuizButton(
}
}
-private val testSpecializations = persistentListOf(
+val specializations = persistentListOf(
CreateQuizState.Loaded.VoSpecialization(
id = 11,
title = "Frontend"
@@ -405,7 +406,7 @@ private val testSpecializations = persistentListOf(
title = "iOS Dev"
),
CreateQuizState.Loaded.VoSpecialization(
- id = 27,
+ id = 21,
title = "Android Dev"
),
CreateQuizState.Loaded.VoSpecialization(
@@ -417,7 +418,7 @@ private val testSpecializations = persistentListOf(
class CreateQuizScreenStateParamProvider : PreviewParameterProvider {
override val values: Sequence = sequenceOf(
CreateQuizState.Loaded(
- specializations = testSpecializations,
+ specializations = specializations,
selectedSpecializationId = 11,
questionsCount = 1
),
@@ -430,7 +431,7 @@ class CreateQuizScreenStateParamProvider : PreviewParameterProvider
+fun DynamicPreviewUI() {
+ val mockDomainList = specializations.map { voSpec ->
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
index 483c5a75..29138a7f 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -33,28 +32,36 @@ import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
import ru.yeahub.interview_trainer.impl.R
+private val FIGMA_HORIZONTAL_PADDING = 16.dp
+private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
+private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
+
+@StaticPreview
@Composable
fun CreateQuizLoading(
- paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
- Box(modifier = modifier.padding(paddingValues)) {
- Column(
- modifier = Modifier.padding(horizontal = 16.dp)
- ) {
- Text(
- modifier = Modifier.padding(vertical = 24.dp),
- style = LocalAppTypography.current.head5,
- text = stringResource(R.string.create_quiz_screen_main_title),
- color = colors.black900
- )
+ Column(
+ modifier = modifier
+ .padding(horizontal = FIGMA_HORIZONTAL_PADDING)
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(vertical = 24.dp),
+ style = LocalAppTypography.current.head5,
+ text = stringResource(R.string.create_quiz_screen_main_title),
+ color = colors.black900
+ )
- PlaceHolderBlock()
+ PlaceHolderBlock()
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
- DisabledStartQuizButton()
- }
+ PlaceHolderBlock()
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ DisabledStartQuizButton()
}
}
@@ -81,7 +88,7 @@ private fun PlaceHolderBlock(
Box(
modifier = Modifier
.fillMaxWidth()
- .height(640.dp)
+ .height(148.dp)
.background(Color.LightGray, shape = RoundedCornerShape(4.dp))
)
}
@@ -93,7 +100,7 @@ private fun DisabledStartQuizButton(
) {
SecondaryButton(
modifier = modifier
- .padding(vertical = 24.dp)
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
.height(48.dp)
.fillMaxWidth(),
enabled = false,
@@ -121,10 +128,4 @@ private fun DisabledStartQuizButton(
)
}
}
-}
-
-@StaticPreview
-@Composable
-internal fun StaticPreviewCreateQuizLoading() {
- CreateQuizLoading(paddingValues = PaddingValues(0.dp))
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index e2beb915..26739f27 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -5,8 +5,6 @@
Выбор специализации
Количество вопросов
Начать
- Уменьшить количество вопросов
- Увеличить количество вопросов
УПС!
Что‑то пошло не так
From 8cfd22f1e45d41b345ff166697b6ef99e336e758 Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Fri, 6 Mar 2026 20:26:08 +0300
Subject: [PATCH 11/28] =?UTF-8?q?=20[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-71:=20=D0=9F=D1=8F=D1=82=D1=8B=D0=B9=20=D1=8D?=
=?UTF-8?q?=D1=82=D0=B0=D0=BF=20CreateQuizScreen.=20QOL=20=D0=B8=D0=B7?=
=?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20(#109)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-71: Добавление нужных текстов для entry-point'а
* ANDR-71: Добавление сущности тренажера в бизнес логике главного экрана
* ANDR-71: Добавление обработки команды по тренажеру
* ANDR-71: Добавлена функция обработки навигации к тренажеру и передается в параметр главного экрана
* ANDR-71: Тест Превью с кнопкой тренажера
* ANDR-71: Использование лямбды навигации для отправки комманд
* ANDR-71: Изменена внешняя функция навигации-входа в фичу тренажера
* ANDR-71: Добавлена реализация InterviewTrainerFeatureImpl. Внутри регистрация графа, обработка навигации назад и обработка навигации к экрану тренировки
* ANDR-71: правки по ktlint'у
* ANDR-71: ScreenMapper теперь класс
* ANDR-71: Константы названий экранов интервью-тренажера
* ANDR-71: Реворк навигации
* ANDR-71: Улучшение экрана. Подключение вьюмодели и команд
* ANDR-71: Вторичные Модули DI
* ANDR-71: Вторичный маппер DI модуль
* ANDR-71: Основной DI модуль экрана
* ANDR-71: Подключение модуля тренажера во все приложение
* ANDR-71: Подключение KOIN модуля
* ANDR-71: Исправлен DI модуль для юзкейса
* ANDR-71: Добавил экрану скролл
* ANDR-71: Навигация с главного экрана на первый экран тренажера (CreateQuizScreen)
* ANDR-71: Создание путей для экрана тренажера
* ANDR-71: Убран хардкод путь экрана CreateQuiz
* ANDR-71: Исправлен класс теста
* ANDR-71: проставлен is
* ANDR-71: добавлен строковой ресурс названия 1 экрана тренажера для прокидывания по навигации
* ANDR-71: используем pathManager для навигации к экрану тренировки
* ANDR-71: handleCommand приватная
* ANDR-71: перегрузка метода получения состояния
* ANDR-71: переименован юзкейс
* ANDR-71: Рефактор DI. Все модули объединены в один
* ANDR-71: коммент к DI
* ANDR-71: Подключение Immutable Collections
* ANDR-71: Удаление наружнего апи интерфейса
* ANDR-71: Оптимизация навигации
* ANDR-71: Навигация теперь принимает не строку, а число айди ресурса
* ANDR-71: исправлены тесты
* ANDR-71: В комментарии вставлены TODO()
* ANDR-71: Обработка ошибок при запросе
* ANDR-71: строковые ресурсы для contentDescription
* ANDR-71: Изменение верстки экрана загрузки; добавление передаваемого параметра отступов
* ANDR-71: небольшой рефактор основного экрана (Box вынесен внутрь экранов, контекст не передается, а создается внутри каждой @Composable, убран хардкод contentDescription)
* ANDR-71: добавлен дополнительный ресурс
* ANDR-71: убрана зависимость от класса TextOrResource (чтоб избавиться от создания контекстов)
* ANDR-71: убран ненужный пустой первичный конструктор
* ANDR-71: Добавлен IO диспатчер на получения списка специализаций
---
.../presentation/CreateQuizScreenMapper.kt | 2 +-
.../presentation/CreateQuizViewModel.kt | 31 +--
.../impl/createQuiz/ui/CreateQuizScreen.kt | 180 +++++++++---------
.../createQuiz/ui/CreateQuizScreenLoading.kt | 49 +++--
.../impl/src/main/res/values/strings.xml | 3 +
5 files changed, 132 insertions(+), 133 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
index 9a578480..1f2b6c1d 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizScreenMapper.kt
@@ -3,7 +3,7 @@ package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import kotlinx.collections.immutable.toImmutableList
import ru.yeahub.interview_trainer.impl.createQuiz.domain.DomainSpecialization
-class CreateQuizScreenMapper() {
+class CreateQuizScreenMapper {
fun getScreenState(
specializations: List,
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
index f0f4ccd6..67194ac4 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizViewModel.kt
@@ -1,10 +1,12 @@
package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -24,21 +26,26 @@ open class CreateQuizViewModel(
questionsCount = MIN_QUESTIONS_COUNT
)
)
+ val specsDef = viewModelScopeSafe.async(Dispatchers.IO) {
+ val request = SpecializationsRequest(page = 1, limit = 99)
+ getSpecializationsListUseCase(request).data
+ }
- val screenState = userInputState
- .map { userInput ->
- val request = SpecializationsRequest(page = 1, limit = 99)
+ val screenState = userInputState.map { userInput ->
+ val specializations = specsDef.await()
- screenMapper.getScreenState(
- specializations = getSpecializationsListUseCase(request).data,
- selectedSpecializationId = userInput.selectedSpecializationId,
- questionsCount = userInput.questionsCount
- )
- }.stateIn(
- scope = viewModelScopeSafe,
- started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
- initialValue = CreateQuizState.Loading
+ screenMapper.getScreenState(
+ specializations = specializations,
+ selectedSpecializationId = userInput.selectedSpecializationId,
+ questionsCount = userInput.questionsCount,
)
+ }.catch { throwable ->
+ emit(screenMapper.getScreenState(throwable))
+ }.stateIn(
+ scope = viewModelScopeSafe,
+ started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
+ initialValue = CreateQuizState.Loading
+ )
private val _commands = MutableSharedFlow()
val commands: SharedFlow = _commands
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index 9a93809d..86bed65f 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -1,6 +1,5 @@
package ru.yeahub.interview_trainer.impl.createQuiz.ui
-import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -29,8 +28,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -67,8 +66,6 @@ import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizScreen
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizState
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewModel
-private val FIGMA_HORIZONTAL_PADDING = 16.dp
-private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
@Composable
@@ -82,7 +79,7 @@ fun CreateQuizScreen(
HandleCommand(
commandFlow = viewModel.commands,
- onResult = { result -> onResult(result) }
+ onResult = onResult
)
ScreenUI(
@@ -107,49 +104,42 @@ private fun ScreenUI(
)
}
) { paddingValues ->
- Box(
- modifier = Modifier
- .padding(paddingValues)
- .verticalScroll(rememberScrollState())
- ) {
- when (state) {
- CreateQuizState.Loading -> CreateQuizLoading()
-
- is CreateQuizState.Error -> {
- ErrorScreen(
- error = state.throwable.localizedMessage,
- errorText = TextOrResource.Resource(R.string.error_screen_text),
- titleText = TextOrResource.Resource(R.string.title_error_screen_text),
- backText = TextOrResource.Resource(R.string.back_error_screen_text),
- unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
- onBack = { onEvent(CreateQuizEvent.OnBackClick) }
- )
- }
-
- is CreateQuizState.Loaded -> BaseCreateQuizScreen(
- specializations = state.specializations,
- selectedSpecializationId = state.selectedSpecializationId,
- questionsCount = state.questionsCount,
- onSpecializationClick = { id ->
- onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
- },
- onPlusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
- },
- onMinusQuestionCountClick = { count ->
- onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
- },
- onStartQuizClick = { specializationId: Long, questionsCount: Int ->
- onEvent(
- CreateQuizEvent.OnStartInterviewQuizClick(
- specializationId = specializationId,
- questionCount = questionsCount,
- )
+ when (state) {
+ CreateQuizState.Loading -> CreateQuizLoading(paddingValues = paddingValues)
+
+ is CreateQuizState.Error -> ErrorScreen(
+ error = state.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = { onEvent(CreateQuizEvent.OnBackClick) }
+ )
+
+ is CreateQuizState.Loaded -> BaseCreateQuizScreen(
+ specializations = state.specializations,
+ selectedSpecializationId = state.selectedSpecializationId,
+ questionsCount = state.questionsCount,
+ onSpecializationClick = { id ->
+ onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
+ },
+ onPlusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnPlusQuestionClick(questionsCount = count))
+ },
+ onMinusQuestionCountClick = { count ->
+ onEvent(CreateQuizEvent.OnMinusQuestionClick(questionsCount = count))
+ },
+ onStartQuizClick = { specializationId: Long, questionsCount: Int ->
+ onEvent(
+ CreateQuizEvent.OnStartInterviewQuizClick(
+ specializationId = specializationId,
+ questionCount = questionsCount,
)
- },
- titleText = TextOrResource.Resource(R.string.create_quiz_screen_main_title)
- )
- }
+ )
+ },
+ titleText = stringResource(R.string.create_quiz_screen_main_title),
+ paddingValues = paddingValues
+ )
}
}
}
@@ -181,47 +171,49 @@ private fun BaseCreateQuizScreen(
onPlusQuestionCountClick: (count: Int) -> Unit,
onMinusQuestionCountClick: (count: Int) -> Unit,
onStartQuizClick: (specializationId: Long, questionsCount: Int) -> Unit,
- titleText: TextOrResource,
+ titleText: String,
+ paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
- val context = LocalContext.current
-
- Column(
+ Box(
modifier = modifier
- .padding(horizontal = FIGMA_HORIZONTAL_PADDING),
+ .padding(paddingValues = paddingValues)
+ .verticalScroll(rememberScrollState())
) {
- Text(
- modifier = Modifier
- .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
- text = titleText.getString(context),
- style = LocalAppTypography.current.head5,
- )
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING),
+ text = titleText,
+ style = LocalAppTypography.current.head5,
+ )
- ChooseSpecializationBlock(
- context = context,
- specializations = specializations,
- selectedSpecializationId = selectedSpecializationId,
- onSpecializationClick = onSpecializationClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_specialization_param_header_text)
- )
+ ChooseSpecializationBlock(
+ specializations = specializations,
+ selectedSpecializationId = selectedSpecializationId,
+ onSpecializationClick = onSpecializationClick,
+ titleText = stringResource(R.string.create_quiz_specialization_param_header_text)
+ )
- Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+ Spacer(modifier = Modifier.height(16.dp))
- ChooseQuestionsCountBlock(
- context = context,
- questionsCount = questionsCount,
- onPlusQuestionCountClick = onPlusQuestionCountClick,
- onMinusQuestionCountClick = onMinusQuestionCountClick,
- titleText = TextOrResource.Resource(R.string.create_quiz_question_count_param_header_text)
- )
+ ChooseQuestionsCountBlock(
+ questionsCount = questionsCount,
+ onPlusQuestionCountClick = onPlusQuestionCountClick,
+ onMinusQuestionCountClick = onMinusQuestionCountClick,
+ titleText = stringResource(R.string.create_quiz_question_count_param_header_text)
+ )
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.weight(1f))
- StartQuizButton(
- specializationId = selectedSpecializationId,
- questionsCount = questionsCount,
- onStartQuizClick = onStartQuizClick,
- )
+ StartQuizButton(
+ specializationId = selectedSpecializationId,
+ questionsCount = questionsCount,
+ onStartQuizClick = onStartQuizClick,
+ )
+ }
}
}
@@ -229,15 +221,14 @@ private fun BaseCreateQuizScreen(
private fun ChooseSpecializationBlock(
specializations: ImmutableList,
selectedSpecializationId: Long,
- context: Context,
onSpecializationClick: (Long) -> Unit,
- titleText: TextOrResource,
+ titleText: String,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
- text = titleText.getString(context),
+ text = titleText,
)
Spacer(modifier = Modifier.height(12.dp))
@@ -269,17 +260,16 @@ private fun ChooseSpecializationBlock(
@Composable
private fun ChooseQuestionsCountBlock(
- context: Context,
questionsCount: Int,
onPlusQuestionCountClick: (count: Int) -> Unit,
onMinusQuestionCountClick: (count: Int) -> Unit,
- titleText: TextOrResource,
+ titleText: String,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
style = LocalAppTypography.current.body3Accent,
- text = titleText.getString(context),
+ text = titleText,
)
Spacer(modifier = Modifier.height(12.dp))
@@ -316,7 +306,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.minus_icon),
- contentDescription = "Decrease questions count",
+ contentDescription = stringResource(R.string.decrease_question_count_content_description),
tint = colors.black600
)
}
@@ -334,7 +324,7 @@ private fun QuestionCounter(
) {
Icon(
painter = painterResource(R.drawable.plus_icon),
- contentDescription = "Increase questions count",
+ contentDescription = stringResource(R.string.increase_question_count_content_description),
tint = colors.black600,
)
}
@@ -362,7 +352,7 @@ private fun StartQuizButton(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "Начать",
+ text = stringResource(R.string.create_quiz_start_quiz_button_text),
style = LocalAppTypography.current.body3Strong,
textAlign = TextAlign.Center,
color = colors.white900
@@ -373,14 +363,14 @@ private fun StartQuizButton(
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(ru.yeahub.ui.R.drawable.arrow_next),
- contentDescription = "Start Interview Quiz Session",
+ contentDescription = stringResource(R.string.start_interview_training_content_description),
tint = colors.white900
)
}
}
}
-val specializations = persistentListOf(
+private val testSpecializations = persistentListOf(
CreateQuizState.Loaded.VoSpecialization(
id = 11,
title = "Frontend"
@@ -406,7 +396,7 @@ val specializations = persistentListOf(
title = "iOS Dev"
),
CreateQuizState.Loaded.VoSpecialization(
- id = 21,
+ id = 27,
title = "Android Dev"
),
CreateQuizState.Loaded.VoSpecialization(
@@ -418,7 +408,7 @@ val specializations = persistentListOf(
class CreateQuizScreenStateParamProvider : PreviewParameterProvider {
override val values: Sequence = sequenceOf(
CreateQuizState.Loaded(
- specializations = specializations,
+ specializations = testSpecializations,
selectedSpecializationId = 11,
questionsCount = 1
),
@@ -431,7 +421,7 @@ class CreateQuizScreenStateParamProvider : PreviewParameterProvider
+internal fun DynamicPreviewUI() {
+ val mockDomainList = testSpecializations.map { voSpec ->
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
index 29138a7f..483c5a75 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -32,36 +33,28 @@ import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
import ru.yeahub.interview_trainer.impl.R
-private val FIGMA_HORIZONTAL_PADDING = 16.dp
-private val FIGMA_VERTICAL_BLOCKS_PADDING = 16.dp
-private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
-
-@StaticPreview
@Composable
fun CreateQuizLoading(
+ paddingValues: PaddingValues,
modifier: Modifier = Modifier,
) {
- Column(
- modifier = modifier
- .padding(horizontal = FIGMA_HORIZONTAL_PADDING)
- ) {
- Text(
- modifier = Modifier
- .padding(vertical = 24.dp),
- style = LocalAppTypography.current.head5,
- text = stringResource(R.string.create_quiz_screen_main_title),
- color = colors.black900
- )
-
- PlaceHolderBlock()
-
- Spacer(modifier = Modifier.height(FIGMA_VERTICAL_BLOCKS_PADDING))
+ Box(modifier = modifier.padding(paddingValues)) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = 24.dp),
+ style = LocalAppTypography.current.head5,
+ text = stringResource(R.string.create_quiz_screen_main_title),
+ color = colors.black900
+ )
- PlaceHolderBlock()
+ PlaceHolderBlock()
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(modifier = Modifier.weight(1f))
- DisabledStartQuizButton()
+ DisabledStartQuizButton()
+ }
}
}
@@ -88,7 +81,7 @@ private fun PlaceHolderBlock(
Box(
modifier = Modifier
.fillMaxWidth()
- .height(148.dp)
+ .height(640.dp)
.background(Color.LightGray, shape = RoundedCornerShape(4.dp))
)
}
@@ -100,7 +93,7 @@ private fun DisabledStartQuizButton(
) {
SecondaryButton(
modifier = modifier
- .padding(vertical = FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING)
+ .padding(vertical = 24.dp)
.height(48.dp)
.fillMaxWidth(),
enabled = false,
@@ -128,4 +121,10 @@ private fun DisabledStartQuizButton(
)
}
}
+}
+
+@StaticPreview
+@Composable
+internal fun StaticPreviewCreateQuizLoading() {
+ CreateQuizLoading(paddingValues = PaddingValues(0.dp))
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index 26739f27..4a2b6030 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -5,6 +5,9 @@
Выбор специализации
Количество вопросов
Начать
+ Уменьшить количество вопросов
+ Увеличить количество вопросов
+ Увеличить количество вопросов
УПС!
Что‑то пошло не так
From 580f882ea3f9c7ca6f7c5e29a949870cb5416967 Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Thu, 12 Mar 2026 17:55:44 +0300
Subject: [PATCH 12/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-59:=20=D0=92=D1=82=D0=BE=D1=80=D0=BE=D0=B9=20?=
=?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF=20InterviewQuizScreen.=20Data=20+=20Do?=
=?UTF-8?q?main=20(#119)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-59: создание сущностей domain слоя
* ANDR-59: реализация репозитория, написание Data to Domain маппера
* ANDR-59: новые response и dto для мок-квиза. Добавление функций в ApiService
* ANDR-59: изменение репозитория, реквеста, респонса и маппера
* ANDR-59: написание теста на DataToDomainMapper
* ANDR-59: убраны дефолт параметры
---
.../java/ru/yeahub/network_api/ApiService.kt | 9 ++
.../models/GetNewMockQuizResponse.kt | 10 ++
.../network_api/models/QuestionAnswerDto.kt | 7 ++
.../yeahub/network_impl/RetrofitApiService.kt | 10 ++
.../data/InterviewQuizDataToDomainMapper.kt | 26 ++++
.../data/InterviewQuizRepositoryImpl.kt | 24 ++++
.../interviewQuiz/domain/DomainQuestion.kt | 7 ++
.../domain/DomainQuestionsListResponse.kt | 6 +
.../domain/GetQuestionsListUseCase.kt | 6 +
.../domain/GetQuestionsListUseCaseImpl.kt | 9 ++
.../domain/InterviewQuizRepositoryApi.kt | 8 ++
.../interviewQuiz/domain/QuestionsRequest.kt | 6 +
.../InterviewQuizDataToDomainMapperTest.kt | 117 ++++++++++++++++++
13 files changed, 245 insertions(+)
create mode 100644 core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
create mode 100644 core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizDataToDomainMapper.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizRepositoryImpl.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestion.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestionsListResponse.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCase.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCaseImpl.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/InterviewQuizRepositoryApi.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/QuestionsRequest.kt
create mode 100644 feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/ApiService.kt b/core/network-api/src/main/java/ru/yeahub/network_api/ApiService.kt
index 5ba0ee70..38c2add2 100644
--- a/core/network-api/src/main/java/ru/yeahub/network_api/ApiService.kt
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/ApiService.kt
@@ -1,6 +1,7 @@
package ru.yeahub.network_api
import ru.yeahub.network_api.models.GetCollectionsResponse
+import ru.yeahub.network_api.models.GetNewMockQuizResponse
import ru.yeahub.network_api.models.GetPublicQuestionResponse
import ru.yeahub.network_api.models.GetPublicQuestionsResponse
import ru.yeahub.network_api.models.GetSkillsResponse
@@ -51,4 +52,12 @@ interface ApiService {
specializationsId: Long,
isFree: Boolean
): GetCollectionsResponse
+
+ suspend fun getQuizMockQuestions(
+ skills: List?,
+ complexity: List?,
+ collection: Int?,
+ limit: Int?,
+ specialization: Int,
+ ): GetNewMockQuizResponse
}
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
new file mode 100644
index 00000000..6584f201
--- /dev/null
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
@@ -0,0 +1,10 @@
+package ru.yeahub.network_api.models
+
+data class GetNewMockQuizResponse(
+ val id: String,
+ val startDate: String,
+ val fullCount: Int,
+ val skills: List,
+ val response: List,
+ val questions: List
+)
\ No newline at end of file
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
new file mode 100644
index 00000000..ac8ed579
--- /dev/null
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
@@ -0,0 +1,7 @@
+package ru.yeahub.network_api.models
+
+data class QuestionAnswerDto(
+ val questionId: Int,
+ val questionTitle: String,
+ val answer: String
+)
diff --git a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
index 1b7deeee..dbfcc56c 100644
--- a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
+++ b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
@@ -5,6 +5,7 @@ import retrofit2.http.Path
import retrofit2.http.Query
import ru.yeahub.network_api.ApiService
import ru.yeahub.network_api.models.GetCollectionsResponse
+import ru.yeahub.network_api.models.GetNewMockQuizResponse
import ru.yeahub.network_api.models.GetPublicQuestionResponse
import ru.yeahub.network_api.models.GetPublicQuestionsResponse
import ru.yeahub.network_api.models.GetSkillsResponse
@@ -61,4 +62,13 @@ interface RetrofitApiService : ApiService {
@Query("specializations") specializationsId: Long,
@Query("isFree") isFree: Boolean
): GetCollectionsResponse
+
+ @GET("/interview-preparation/quizzes/mock/new")
+ override suspend fun getQuizMockQuestions(
+ @Query("skills") skills: List?,
+ @Query("complexity") complexity: List?,
+ @Query("collection") collection: Int?,
+ @Query("limit") limit: Int?,
+ @Query("specialization") specialization: Int,
+ ): GetNewMockQuizResponse
}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizDataToDomainMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizDataToDomainMapper.kt
new file mode 100644
index 00000000..2ca65b24
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizDataToDomainMapper.kt
@@ -0,0 +1,26 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.data
+
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestion
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestionsListResponse
+import ru.yeahub.network_api.models.GetNewMockQuizResponse
+import ru.yeahub.network_api.models.GetQuestionResponse
+
+class InterviewQuizDataToDomainMapper {
+
+ fun mapDataListToDomainList(
+ dataResponse: GetNewMockQuizResponse
+ ): DomainQuestionsListResponse {
+ return DomainQuestionsListResponse(
+ fullCount = dataResponse.fullCount,
+ questions = dataResponse.questions.map { item -> dataToDomain(item) }
+ )
+ }
+
+ private fun dataToDomain(data: GetQuestionResponse): DomainQuestion {
+ return DomainQuestion(
+ id = data.id,
+ title = data.title,
+ shortAnswer = data.shortAnswer ?: ""
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizRepositoryImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizRepositoryImpl.kt
new file mode 100644
index 00000000..060156e3
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/data/InterviewQuizRepositoryImpl.kt
@@ -0,0 +1,24 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.data
+
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestionsListResponse
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.InterviewQuizRepositoryApi
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.QuestionsRequest
+import ru.yeahub.network_api.NetworkProvider
+
+class InterviewQuizRepositoryImpl(
+ private val networkProvider: NetworkProvider,
+ private val mapper: InterviewQuizDataToDomainMapper
+) : InterviewQuizRepositoryApi {
+
+ override suspend fun getQuestionsList(
+ request: QuestionsRequest
+ ): DomainQuestionsListResponse = mapper.mapDataListToDomainList(
+ dataResponse = networkProvider.apiService.getQuizMockQuestions(
+ skills = null,
+ complexity = null,
+ collection = null,
+ limit = request.limit,
+ specialization = request.specialization
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestion.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestion.kt
new file mode 100644
index 00000000..ca10ee32
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestion.kt
@@ -0,0 +1,7 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+data class DomainQuestion(
+ val id: Long,
+ val title: String,
+ val shortAnswer: String
+)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestionsListResponse.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestionsListResponse.kt
new file mode 100644
index 00000000..b7552503
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/DomainQuestionsListResponse.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+data class DomainQuestionsListResponse(
+ val fullCount: Int,
+ val questions: List
+)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCase.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCase.kt
new file mode 100644
index 00000000..df2f42a9
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCase.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+interface GetQuestionsListUseCase {
+
+ suspend operator fun invoke(request: QuestionsRequest): DomainQuestionsListResponse
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCaseImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCaseImpl.kt
new file mode 100644
index 00000000..7ca52819
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/GetQuestionsListUseCaseImpl.kt
@@ -0,0 +1,9 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+class GetQuestionsListUseCaseImpl(
+ private val repository: InterviewQuizRepositoryApi
+) : GetQuestionsListUseCase {
+
+ override suspend fun invoke(request: QuestionsRequest) =
+ repository.getQuestionsList(request)
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/InterviewQuizRepositoryApi.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/InterviewQuizRepositoryApi.kt
new file mode 100644
index 00000000..74831c7b
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/InterviewQuizRepositoryApi.kt
@@ -0,0 +1,8 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+interface InterviewQuizRepositoryApi {
+
+ suspend fun getQuestionsList(
+ request: QuestionsRequest
+ ): DomainQuestionsListResponse
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/QuestionsRequest.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/QuestionsRequest.kt
new file mode 100644
index 00000000..2c02a741
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/domain/QuestionsRequest.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.domain
+
+data class QuestionsRequest(
+ val limit: Int,
+ val specialization: Int,
+)
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
new file mode 100644
index 00000000..af02ea37
--- /dev/null
+++ b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
@@ -0,0 +1,117 @@
+package test
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.interview_trainer.impl.interviewQuiz.data.InterviewQuizDataToDomainMapper
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestion
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestionsListResponse
+import ru.yeahub.network_api.models.GetNewMockQuizResponse
+import ru.yeahub.network_api.models.GetQuestionResponse
+import ru.yeahub.network_api.models.NestedUserReferenceDto
+import ru.yeahub.test.TestArgumentsProvider
+
+class InterviewQuizDataToDomainMapperTest {
+
+ private val dataToDomainMapper = InterviewQuizDataToDomainMapper()
+
+ @ParameterizedTest
+ @ArgumentsSource(ArgumentsProvider::class)
+ fun dataToDomainMappingTestCase(
+ testCase: DataToDomainMappingTestCase
+ ) {
+ val result = dataToDomainMapper.mapDataListToDomainList(testCase.dataToTest)
+ assertEquals(testCase.expectedResult, result)
+ }
+
+ object QuestionExamplesDataClasses {
+
+ private val defaultQuestionResponse = GetQuestionResponse(
+ id = 1,
+ title = "default question 0",
+ description = "default description",
+ code = "0101",
+ imageSrc = null,
+ keywords = null,
+ longAnswer = null,
+ shortAnswer = "default short answer",
+ status = null,
+ rate = null,
+ complexity = null,
+ createdById = "07.03.2026",
+ updatedById = null,
+ questionSpecializations = emptyList(),
+ questionSkills = emptyList(),
+ createdAt = "07.03.2026",
+ updatedAt = "07.03.2026",
+ createdBy = NestedUserReferenceDto("userId", "username"),
+ updatedBy = null
+ )
+
+ private val defaultDomainQuestion = DomainQuestion(
+ id = 1,
+ title = "default question 0",
+ shortAnswer = "default short answer"
+ )
+
+ private val defaultQuestionResponseWithLongAnswer = GetQuestionResponse(
+ id = 2,
+ title = "default question with long answer 1",
+ description = "default description",
+ code = "0102",
+ imageSrc = null,
+ keywords = null,
+ longAnswer = "default long answer",
+ shortAnswer = "default short answer",
+ status = null,
+ rate = null,
+ complexity = null,
+ createdById = "07.03.2026",
+ updatedById = null,
+ questionSpecializations = emptyList(),
+ questionSkills = emptyList(),
+ createdAt = "07.03.2026",
+ updatedAt = "07.03.2026",
+ createdBy = NestedUserReferenceDto("userId", "username"),
+ updatedBy = null
+ )
+
+ private val defaultDomainQuestionWithLongAnswer = DomainQuestion(
+ id = 2,
+ title = "default question with long answer 1",
+ shortAnswer = "default short answer"
+ )
+
+ val defaultNewMockQuizResponse = GetNewMockQuizResponse(
+ id = "0101010",
+ startDate = "01.01.2026",
+ fullCount = 2,
+ skills = emptyList(),
+ response = emptyList(),
+ questions = listOf(defaultQuestionResponse, defaultQuestionResponseWithLongAnswer),
+ )
+
+ val defaultDomainListResponse = DomainQuestionsListResponse(
+ fullCount = 2,
+ questions = listOf(defaultDomainQuestion, defaultDomainQuestionWithLongAnswer)
+ )
+ }
+
+ data class DataToDomainMappingTestCase(
+ val dataToTest: GetNewMockQuizResponse,
+ val expectedResult: DomainQuestionsListResponse
+ )
+
+ class ArgumentsProvider:
+ TestArgumentsProvider() {
+
+ override fun testCases(): List {
+ return listOf(
+ DataToDomainMappingTestCase(
+ dataToTest = QuestionExamplesDataClasses.defaultNewMockQuizResponse,
+ expectedResult = QuestionExamplesDataClasses.defaultDomainListResponse
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
From 27d9524544d5573de36a3b36e30e290c0b4bfa8a Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Sat, 21 Mar 2026 19:06:43 +0300
Subject: [PATCH 13/28] =?UTF-8?q?ANDR-93:=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81?=
=?UTF-8?q?=20=D0=B2=20core-ui=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE=D0=BA=20?=
=?UTF-8?q?=D0=97=D0=BD=D0=B0=D1=8E/=D0=9D=D0=B5=20=D0=B7=D0=BD=D0=B0?=
=?UTF-8?q?=D1=8E.=20=D0=98=D1=81=D0=BF=D0=BE=D0=BB=D0=B7=D0=BE=D0=B2?=
=?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=20QuizScreen=20(#128)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../core_ui/component/QuizAnswerButton.kt | 151 ++++++++++++++++++
.../main/res/drawable}/thumbs_down_icon.xml | 0
.../src/main/res/drawable}/thumbs_up_icon.xml | 0
core/ui/src/main/res/values/strings.xml | 3 +
.../interviewQuiz/ui/InterviewQuizScreen.kt | 67 ++------
.../impl/src/main/res/values/strings.xml | 2 -
6 files changed, 164 insertions(+), 59 deletions(-)
create mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/component/QuizAnswerButton.kt
rename {feature/interview-trainer/impl/src/main/res/drawable-nodpi => core/ui/src/main/res/drawable}/thumbs_down_icon.xml (100%)
rename {feature/interview-trainer/impl/src/main/res/drawable-nodpi => core/ui/src/main/res/drawable}/thumbs_up_icon.xml (100%)
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/QuizAnswerButton.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/QuizAnswerButton.kt
new file mode 100644
index 00000000..9ead9fe4
--- /dev/null
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/QuizAnswerButton.kt
@@ -0,0 +1,151 @@
+package ru.yeahub.core_ui.component
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.Theme
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.ui.R
+
+private val FIGMA_LOW_PADDING = 8.dp
+private val FIGMA_RADIUS = 12.dp
+
+@Composable
+fun KnownAnswerButton(
+ enabled: Boolean,
+ isHighlighted: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ QuizAnswerButton(
+ iconRes = R.drawable.thumbs_up_icon,
+ text = TextOrResource.Resource(R.string.quiz_answer_known),
+ enabled = enabled,
+ isHighlighted = isHighlighted,
+ onClick = onClick,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun UnknownAnswerButton(
+ enabled: Boolean,
+ isHighlighted: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ QuizAnswerButton(
+ iconRes = R.drawable.thumbs_down_icon,
+ text = TextOrResource.Resource(R.string.quiz_answer_unknown),
+ enabled = enabled,
+ isHighlighted = isHighlighted,
+ onClick = onClick,
+ modifier = modifier
+ )
+}
+
+@Composable
+private fun QuizAnswerButton(
+ iconRes: Int,
+ text: TextOrResource,
+ enabled: Boolean,
+ isHighlighted: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ val contentColor = if (isHighlighted) {
+ Theme.colors.purple700
+ } else {
+ Theme.colors.black700
+ }
+
+ Button(
+ onClick = onClick,
+ modifier = modifier
+ .width(120.dp)
+ .height(48.dp),
+ shape = RoundedCornerShape(FIGMA_RADIUS),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Theme.colors.black10,
+ contentColor = contentColor,
+ disabledContainerColor = Theme.colors.black10,
+ disabledContentColor = contentColor
+ ),
+ contentPadding = PaddingValues(
+ horizontal = 12.dp,
+ vertical = FIGMA_LOW_PADDING
+ ),
+ enabled = enabled
+ ) {
+ Icon(
+ painter = painterResource(iconRes),
+ contentDescription = null,
+ modifier = Modifier.padding(end = FIGMA_LOW_PADDING)
+ )
+ Text(
+ text = when (text) {
+ is TextOrResource.Text -> text.text
+ is TextOrResource.Resource -> text.getString(context)
+ },
+ style = Theme.typography.body2
+ )
+ }
+}
+
+@StaticPreview
+@Composable
+fun KnownAnswerHighlightedButtonPreview() {
+ KnownAnswerButton(
+ enabled = true,
+ isHighlighted = true,
+ onClick = {},
+ modifier = Modifier
+ )
+}
+
+@StaticPreview
+@Composable
+fun KnownAnswerButtonPreview() {
+ KnownAnswerButton(
+ enabled = true,
+ isHighlighted = false,
+ onClick = {},
+ modifier = Modifier
+ )
+}
+
+@StaticPreview
+@Composable
+fun UnknownAnswerHighlightedButtonPreview() {
+ UnknownAnswerButton(
+ enabled = true,
+ isHighlighted = true,
+ onClick = {},
+ modifier = Modifier
+ )
+}
+
+@StaticPreview
+@Composable
+fun UnknownAnswerButtonPreview() {
+ UnknownAnswerButton(
+ enabled = true,
+ isHighlighted = true,
+ onClick = {},
+ modifier = Modifier
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml b/core/ui/src/main/res/drawable/thumbs_down_icon.xml
similarity index 100%
rename from feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_down_icon.xml
rename to core/ui/src/main/res/drawable/thumbs_down_icon.xml
diff --git a/feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml b/core/ui/src/main/res/drawable/thumbs_up_icon.xml
similarity index 100%
rename from feature/interview-trainer/impl/src/main/res/drawable-nodpi/thumbs_up_icon.xml
rename to core/ui/src/main/res/drawable/thumbs_up_icon.xml
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index 1b7c1a34..a22fce8d 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -27,4 +27,7 @@
Интервью тренажер
Улучшите свои знания перед собеседованием
+
+ Не знаю
+ Знаю
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
index 230a248c..72cb22d5 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledIconButton
@@ -50,9 +48,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import ru.yeahub.core_ui.component.ErrorScreen
+import ru.yeahub.core_ui.component.KnownAnswerButton
import ru.yeahub.core_ui.component.PrimaryButton
import ru.yeahub.core_ui.component.SecondaryButton
import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
+import ru.yeahub.core_ui.component.UnknownAnswerButton
import ru.yeahub.core_ui.component.YeahubButtonDefaults
import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.Theme
@@ -283,18 +283,16 @@ private fun QuestionCard(
Spacer(Modifier.height(FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING))
Row(Modifier.padding(bottom = FIGMA_MEDIUM_PADDING)) {
- QuizAnswerButton(
- painter = painterResource(R.drawable.thumbs_down_icon),
- text = TextOrResource.Resource(R.string.quiz_answer_unknown),
- onClick = onUnknownClick,
- isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN
+ UnknownAnswerButton(
+ enabled = true,
+ isHighlighted = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ onClick = onUnknownClick
)
Spacer(Modifier.weight(1f))
- QuizAnswerButton(
- painter = painterResource(R.drawable.thumbs_up_icon),
- text = TextOrResource.Resource(R.string.quiz_answer_known),
- onClick = onKnownClick,
- isSelected = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ KnownAnswerButton(
+ enabled = true,
+ isHighlighted = state.selectedAnswer == InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ onClick = onKnownClick
)
}
HorizontalDivider(
@@ -408,51 +406,6 @@ private fun NavigationButton(
}
}
-@Composable
-private fun QuizAnswerButton(
- painter: Painter,
- text: TextOrResource,
- onClick: () -> Unit,
- isSelected: Boolean
-) {
- val context = LocalContext.current
-
- val contentColor = if (isSelected) {
- Theme.colors.purple700
- } else {
- Theme.colors.black700
- }
-
- Button(
- onClick = onClick,
- modifier = Modifier
- .width(120.dp)
- .height(48.dp),
- shape = RoundedCornerShape(FIGMA_RADIUS),
- colors = ButtonDefaults.buttonColors(
- containerColor = Theme.colors.black10,
- contentColor = contentColor
- ),
- contentPadding = PaddingValues(
- horizontal = 12.dp,
- vertical = FIGMA_LOW_PADDING
- )
- ) {
- Icon(
- painter = painter,
- contentDescription = null,
- modifier = Modifier.padding(end = FIGMA_LOW_PADDING)
- )
- Text(
- text = when (text) {
- is TextOrResource.Text -> text.text
- is TextOrResource.Resource -> text.getString(context)
- },
- style = Theme.typography.body2
- )
- }
-}
-
private val shortAnswerForPreview = "Виртуальный DOM (VDOM) — это легковесное " +
"представление реального DOM в памяти, которое используется в " +
"JavaScript-библиотеках, таких как React и Vue, " +
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index 4a2b6030..b0d3afa4 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -16,8 +16,6 @@
Проверить результаты
Свернуть ответ
Показать ответ
- Не знаю
- Знаю
Завершить
Назад
Далее
From b7e8bd45d0bcc1cc9e2547066417940dae3a091e Mon Sep 17 00:00:00 2001
From: arte123ras
Date: Thu, 26 Mar 2026 20:30:22 +0300
Subject: [PATCH 14/28] =?UTF-8?q?Feature/ANDR-63-1=20=D0=9F=D1=83=D0=B1?=
=?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD?=
=?UTF-8?q?=D0=B0=D0=B6=D0=B5=D1=80,=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?=
=?UTF-8?q?=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=BE?=
=?UTF-8?q?=D0=B2.=20=D0=92=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0=20=D1=8D?=
=?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20(#130)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-63
- Добавление ViewModel
* ANDR-63
- Добавление стейтов
* ANDR-63:
- Добавление строковых ресурсов
* ANDR-63:
- Добавление скринмаппера
* ANDR-63:
- Загрузка экрана
* ANDR-63:
- Полная верстка экрана
* ANDR-63:
- Интерфейс событий
* ANDR-63:
- Убрал закардхорженный текст
* ANDR-63:
- Добавил Compose элементы ивана с кнопками знаю/не знаю
* ANDR-63:
- Убрал лишние параметры из TitleSection и SkillProgressRow
* ANDR-63:
- убрал ивенты
* ANDR-63:
- Изменил логику, теперь без двух стейтов
* ANDR-63:
- Изменил на PersistentList
* ANDR-63:
- Изменил на в маппере скиллы также на persistentlist
* ANDR-63:
- набольшие правки в экране, связанные с изменением других файлов
* ANDR-63:
- Малые правки для ktlint и добавление пробела между ресурсами
* ANDR-63: Изменил ViewModel на BaseViewModel
* ANDR-63: Оптимизация ktlint
* ANDR-63: Удалил лишние параметры
* ANDR-63: Оптимизация ktlint
* ANDR-63: Удалил параметр заголовка и прокинул через стейт
* ANDR-63: Оптимизация ktlint
* ANDR-63: Правки по дизайну, в частности SkillProgressRow
---
.../InterviewQuizResultEvent.kt | 6 +
.../InterviewQuizResultScreenMapper.kt | 43 ++
.../InterviewQuizResultState.kt | 45 ++
.../InterviewQuizResultViewModel.kt | 29 +
.../ui/InterviewQuizResultScreen.kt | 513 ++++++++++++++++++
.../ui/InterviewQuizResultScreenLoading.kt | 154 ++++++
.../impl/src/main/res/values/strings.xml | 12 +
7 files changed, 802 insertions(+)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreenLoading.kt
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt
new file mode 100644
index 00000000..7f3e37e5
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt
@@ -0,0 +1,6 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult
+
+sealed interface InterviewQuizResultEvent {
+
+ data object Todo : InterviewQuizResultEvent // TODO: сделать ивенты
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
new file mode 100644
index 00000000..26ccd35a
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
@@ -0,0 +1,43 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult
+
+import InterviewQuizResultState
+import kotlinx.collections.immutable.persistentListOf
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+
+private const val DEFAULT_PERCENTAGE = 0.75f
+private const val DEFAULT_TOTAL_QUESTIONS = 20
+private const val DEFAULT_NEW_QUESTIONS = 50
+private const val DEFAULT_IN_PROGRESS = 120
+private const val DEFAULT_STUDIED = 12
+private const val DEFAULT_SKILL_SCORE = 60
+private const val DEFAULT_SKILL_MAX = 120
+
+class InterviewQuizResultScreenMapper {
+
+ fun getScreenState(): InterviewQuizResultState =
+ InterviewQuizResultState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ overallPercentage = DEFAULT_PERCENTAGE,
+ totalQuestions = DEFAULT_TOTAL_QUESTIONS,
+ newQuestions = DEFAULT_NEW_QUESTIONS,
+ inProgress = DEFAULT_IN_PROGRESS,
+ studied = DEFAULT_STUDIED,
+ skills = persistentListOf(
+ InterviewQuizResultState.Loaded.VoSkillStat("HTML", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
+ InterviewQuizResultState.Loaded.VoSkillStat("CSS", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
+ InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
+ InterviewQuizResultState.Loaded.VoSkillStat("React", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
+ InterviewQuizResultState.Loaded.VoSkillStat("PHP", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
+ InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX)
+ ),
+ questions = persistentListOf(
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false),
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false),
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
+ InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false)
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt
new file mode 100644
index 00000000..7fd0c6ea
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt
@@ -0,0 +1,45 @@
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.PersistentList
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+
+@Immutable
+sealed interface InterviewQuizResultState {
+
+ val titleTopAppBar: TextOrResource
+
+ data object Loading : InterviewQuizResultState {
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
+
+ data class Loaded(
+ override val titleTopAppBar: TextOrResource,
+ val overallPercentage: Float,
+ val totalQuestions: Int,
+ val newQuestions: Int,
+ val inProgress: Int,
+ val studied: Int,
+ val skills: PersistentList,
+ val questions: PersistentList
+ ) : InterviewQuizResultState {
+
+ @Immutable
+ data class VoSkillStat(
+ val name: String,
+ val current: Int,
+ val max: Int
+ )
+
+ @Immutable
+ data class VoQuestionResult(
+ val questionText: String,
+ val isCorrect: Boolean
+ )
+ }
+
+ data class Error(
+ val throwable: Throwable
+ ) : InterviewQuizResultState {
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
new file mode 100644
index 00000000..cf5ea388
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
@@ -0,0 +1,29 @@
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
+import ru.yeahub.core_utils.BaseViewModel
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultEvent
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultScreenMapper
+
+private const val STOP_TIMEOUT_MILLIS = 5000L
+
+class InterviewQuizResultViewModel(
+ private val mapper: InterviewQuizResultScreenMapper
+) : BaseViewModel() {
+
+ val state: StateFlow =
+ flow {
+ emit(mapper.getScreenState())
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
+ initialValue = InterviewQuizResultState.Loading
+ )
+
+ fun onEvent(event: InterviewQuizResultEvent) {
+ // TODO:
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
new file mode 100644
index 00000000..5a2f4be7
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
@@ -0,0 +1,513 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.ui
+
+import InterviewQuizResultState
+import InterviewQuizResultViewModel
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.collections.immutable.persistentListOf
+import org.koin.androidx.compose.koinViewModel
+import ru.yeahub.core_ui.component.ErrorScreen
+import ru.yeahub.core_ui.component.KnownAnswerButton
+import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
+import ru.yeahub.core_ui.component.UnknownAnswerButton
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+import ru.yeahub.core_ui.theme.Theme.typography
+import ru.yeahub.core_ui.theme.colors
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultEvent
+
+private val H_PADDING = 16.dp
+private val V_BLOCK = 16.dp
+private val V_SECTION = 32.dp
+private val V_TINY = 8.dp
+private val CARD_RADIUS = 12.dp
+private val CARD_ELEVATION = 4.dp
+private val SKILL_BAR_HEIGHT = 12.dp
+
+@Composable
+fun InterviewQuizResultScreen() {
+ val viewModel: InterviewQuizResultViewModel = koinViewModel()
+ val state = viewModel.state.collectAsStateWithLifecycle()
+
+ ScreenUI(
+ state = state,
+ onEvent = viewModel::onEvent
+ )
+}
+
+@Composable
+private fun ScreenUI(
+ state: State,
+ onEvent: (InterviewQuizResultEvent) -> Unit
+) {
+ Scaffold(
+ containerColor = colors.black10,
+ topBar = {
+ TopAppBarWithBottomBorder(
+ title = state.value.titleTopAppBar,
+ onBackClick = { }
+ )
+ }
+ ) { innerPadding ->
+ when (val currentState = state.value) {
+ is InterviewQuizResultState.Loading -> {
+ InterviewQuizResultLoading()
+ }
+
+ is InterviewQuizResultState.Error -> {
+ ErrorScreen(
+ error = currentState.throwable.localizedMessage,
+ errorText = TextOrResource.Resource(R.string.error_screen_text),
+ titleText = TextOrResource.Resource(R.string.title_error_screen_text),
+ backText = TextOrResource.Resource(R.string.back_error_screen_text),
+ unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
+ onBack = {}
+ )
+ }
+ is InterviewQuizResultState.Loaded -> {
+ BaseInterviewQuizResultScreen(
+ state = currentState,
+ innerPadding = innerPadding,
+ onEvent = onEvent
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun BaseInterviewQuizResultScreen(
+ state: InterviewQuizResultState.Loaded,
+ innerPadding: PaddingValues,
+ onEvent: (InterviewQuizResultEvent) -> Unit
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(scrollState)
+ .padding(horizontal = H_PADDING),
+ verticalArrangement = Arrangement.spacedBy(V_SECTION)
+ ) {
+ TitleSection()
+
+ Card(
+ shape = RoundedCornerShape(CARD_RADIUS),
+ elevation = CardDefaults.cardElevation(CARD_ELEVATION),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(H_PADDING)) {
+ Text(
+ text = stringResource(R.string.interview_quiz_result_statistics),
+ style = typography.body3Accent.copy(fontWeight = FontWeight.Bold),
+ color = colors.black900
+ )
+ Spacer(Modifier.height(V_BLOCK))
+ OverallProgressStatistics(
+ percentage = state.overallPercentage.coerceIn(0f, 1f),
+ totalQuestions = state.totalQuestions,
+ newQuestions = state.newQuestions,
+ inProgress = state.inProgress,
+ studied = state.studied,
+ )
+
+ Spacer(Modifier.height(16.dp))
+
+ Text(
+ text = stringResource(R.string.interview_quiz_result_progress),
+ style = typography.body3Strong,
+ color = colors.black900
+ )
+ Spacer(Modifier.height(13.dp))
+
+ state.skills.forEach { skill ->
+ SkillProgressRow(skill)
+ }
+
+ Spacer(Modifier.height(V_TINY))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ Text(
+ text = stringResource(R.string.interview_quiz_result_show_all),
+ style = typography.body3,
+ color = colors.purple700,
+ modifier = Modifier.clickable {
+ onEvent(InterviewQuizResultEvent.Todo)
+ }
+ )
+ }
+ }
+ }
+ Card(
+ shape = RoundedCornerShape(CARD_RADIUS),
+ elevation = CardDefaults.cardElevation(CARD_ELEVATION),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier.padding(H_PADDING)
+ ) {
+ Text(
+ text = stringResource(R.string.interview_quiz_result_questuions),
+ style = typography.body4,
+ color = colors.black900,
+ modifier = Modifier.padding(bottom = 12.dp)
+ )
+
+ state.questions.forEach { question ->
+ QuestionItem(question)
+ }
+ }
+ }
+ Spacer(Modifier.height(V_SECTION))
+ }
+}
+
+@Composable
+private fun TitleSection() {
+ Text(
+ text = stringResource(R.string.interview_quiz_result_result),
+ style = typography.head5,
+ color = colors.black900,
+ modifier = Modifier.padding(top = 24.dp)
+ )
+}
+
+@Composable
+private fun SkillProgressRow(skill: InterviewQuizResultState.Loaded.VoSkillStat) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = skill.name,
+ style = typography.body3Strong,
+ color = colors.black900
+ )
+
+ Text(
+ text = "${skill.current}/${skill.max}".trim(),
+ style = typography.body3Strong,
+ color = colors.black900,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ LinearProgressIndicator(
+ progress = { skill.current.toFloat() / skill.max },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(SKILL_BAR_HEIGHT)
+ .clip(RoundedCornerShape(4.dp)),
+ color = colors.purple700,
+ trackColor = colors.purple400,
+ strokeCap = StrokeCap.Round,
+ gapSize = (-10).dp,
+ drawStopIndicator = {}
+ )
+ }
+}
+
+@Composable
+private fun StatItem(
+ label: String,
+ value: String
+) {
+ Card(
+ modifier = Modifier
+ .widthIn(min = 72.dp)
+ .height(60.dp),
+ shape = RoundedCornerShape(12.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White)
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = label,
+ style = typography.body2,
+ color = colors.black400,
+ maxLines = 1
+ )
+
+ Text(
+ text = value,
+ style = typography.body3Strong,
+ color = colors.black800
+ )
+ }
+ }
+}
+
+@Composable
+private fun OverallProgressStatistics(
+ percentage: Float,
+ totalQuestions: Int,
+ newQuestions: Int,
+ inProgress: Int,
+ studied: Int
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(Modifier.height(8.dp))
+
+ Box(
+ modifier = Modifier.size(250.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ color = colors.yellow600.copy(alpha = 0.15f),
+ shape = CircleShape
+ )
+ )
+
+ CircularProgressIndicator(
+ progress = { 1f },
+ modifier = Modifier.fillMaxSize(),
+ color = colors.yellow600.copy(alpha = 0.5f),
+ strokeWidth = 24.dp,
+ strokeCap = StrokeCap.Butt,
+ trackColor = Color.Transparent
+ )
+
+ CircularProgressIndicator(
+ progress = { percentage },
+ modifier = Modifier.fillMaxSize(),
+ color = colors.green800,
+ strokeWidth = 24.dp,
+ strokeCap = StrokeCap.Round,
+ trackColor = Color.Transparent
+ )
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = "${(percentage * 100).toInt()}%",
+ style = typography.head4.copy(fontWeight = FontWeight.Bold),
+ color = colors.black700
+ )
+ Text(
+ text = stringResource(R.string.interview_quiz_result_done),
+ style = typography.body4,
+ color = colors.black700
+ )
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ StatItem(
+ stringResource(R.string.interview_quiz_result_overall),
+ totalQuestions.toString()
+ )
+ StatItem(
+ stringResource(R.string.interview_quiz_result_new),
+ newQuestions.toString()
+ )
+ StatItem(
+ stringResource(R.string.interview_quiz_result_in_progress),
+ inProgress.toString()
+ )
+ StatItem(
+ stringResource(R.string.interview_quiz_result_studied),
+ studied.toString()
+ )
+ }
+ }
+}
+
+@Composable
+private fun QuestionItem(
+ question: InterviewQuizResultState.Loaded.VoQuestionResult
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp),
+ shape = RoundedCornerShape(CARD_RADIUS),
+ elevation = CardDefaults.cardElevation(2.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(H_PADDING),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(104.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color(0xFF0F1C2E))
+ ) {
+ Image(
+ painter = painterResource(id = ru.yeahub.ui.R.drawable.question_image),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = question.questionText,
+ style = typography.body3Strong,
+ color = colors.black900,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color(0xFFF3F3F3))
+ .padding(horizontal = 12.dp, vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val isCorrect = question.isCorrect
+
+ if (isCorrect) {
+ KnownAnswerButton(
+ enabled = false,
+ isHighlighted = true,
+ onClick = {}
+ )
+ } else {
+ UnknownAnswerButton(
+ enabled = false,
+ isHighlighted = false,
+ onClick = {}
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@StaticPreview
+@Composable
+internal fun InterviewQuizResultScreenPreview(
+ @PreviewParameter(InterviewQuizResultStateParamProvider::class)
+ state: InterviewQuizResultState,
+) {
+ ScreenUI(
+ state = rememberUpdatedState(state),
+ onEvent = {}
+ )
+}
+
+class InterviewQuizResultStateParamProvider :
+ PreviewParameterProvider {
+
+ override val values = sequenceOf(
+ InterviewQuizResultState.Loaded(
+ overallPercentage = 0.75f,
+ totalQuestions = 20,
+ newQuestions = 50,
+ inProgress = 120,
+ studied = 12,
+ skills = persistentListOf(
+ InterviewQuizResultState.Loaded.VoSkillStat("HTML", 60, 120),
+ InterviewQuizResultState.Loaded.VoSkillStat("CSS", 60, 120),
+ InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", 60, 120),
+ InterviewQuizResultState.Loaded.VoSkillStat("React", 60, 120),
+ InterviewQuizResultState.Loaded.VoSkillStat("PHP", 60, 120),
+ InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", 60, 120)
+ ),
+ questions = persistentListOf(
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ "Что такое Virtual DOM, и как он работает?",
+ false
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ "Что такое Virtual DOM, и как он работает?",
+ true
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ "Что такое Virtual DOM, и как он работает?",
+ false
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ "Что такое Virtual DOM, и как он работает?",
+ true
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ "Что такое Virtual DOM, и как он работает?",
+ false
+ )
+ ),
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreenLoading.kt
new file mode 100644
index 00000000..7745a5b1
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreenLoading.kt
@@ -0,0 +1,154 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.valentinilk.shimmer.shimmer
+import ru.yeahub.core_ui.example.staticPreview.StaticPreview
+
+private const val TITLE_WIDTH_FACTOR = 0.7f
+private const val SKILLS_COUNT = 4
+private const val SUBTITLE_WIDTH_FACTOR = 0.6f
+private const val ANSWERS_COUNT = 6
+private const val ANSWER_MAIN_WEIGHT = 1f
+private const val ANSWER_SECONDARY_WEIGHT = 3f
+private const val CARDS_COUNT = 5
+private val SHIMMER_COLOR = Color.LightGray
+private const val CARD_PLACEHOLDER_COLOR_HEX = 0xFF1E1E2E
+private val CARD_PLACEHOLDER_COLOR = Color(CARD_PLACEHOLDER_COLOR_HEX)
+
+@StaticPreview
+@Composable
+fun InterviewQuizResultLoading() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .padding(horizontal = 16.dp)
+ ) {
+ Spacer(Modifier.height(24.dp))
+
+ Box(
+ modifier = Modifier
+ .height(32.dp)
+ .fillMaxWidth(TITLE_WIDTH_FACTOR)
+ .background(SHIMMER_COLOR, RoundedCornerShape(4.dp))
+ .shimmer()
+ )
+
+ Spacer(Modifier.height(16.dp))
+
+ Box(
+ modifier = Modifier
+ .size(200.dp)
+ .align(Alignment.CenterHorizontally)
+ .background(SHIMMER_COLOR, RoundedCornerShape(100.dp))
+ .shimmer()
+ )
+
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ repeat(SKILLS_COUNT) {
+ Column {
+ Box(
+ Modifier
+ .size(40.dp, 24.dp)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ Spacer(Modifier.height(4.dp))
+ Box(
+ Modifier
+ .size(60.dp, 16.dp)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ }
+ }
+ }
+
+ Spacer(Modifier.height(32.dp))
+
+ Box(
+ Modifier
+ .height(28.dp)
+ .fillMaxWidth(SUBTITLE_WIDTH_FACTOR)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+
+ Spacer(Modifier.height(12.dp))
+
+ repeat(ANSWERS_COUNT) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp)
+ ) {
+ Box(
+ Modifier
+ .weight(ANSWER_MAIN_WEIGHT)
+ .height(20.dp)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ Spacer(Modifier.width(12.dp))
+ Box(
+ Modifier
+ .weight(ANSWER_SECONDARY_WEIGHT)
+ .height(12.dp)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ Spacer(Modifier.width(12.dp))
+ Box(
+ Modifier
+ .size(60.dp, 20.dp)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ }
+ }
+ Spacer(Modifier.height(32.dp))
+ Box(
+ Modifier
+ .height(28.dp)
+ .fillMaxWidth(TITLE_WIDTH_FACTOR)
+ .background(SHIMMER_COLOR)
+ .shimmer()
+ )
+ Spacer(Modifier.height(12.dp))
+ repeat(CARDS_COUNT) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(72.dp)
+ .shimmer(),
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(containerColor = CARD_PLACEHOLDER_COLOR)
+ ) {}
+ Spacer(Modifier.height(12.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/res/values/strings.xml b/feature/interview-trainer/impl/src/main/res/values/strings.xml
index b0d3afa4..40e1bafc 100644
--- a/feature/interview-trainer/impl/src/main/res/values/strings.xml
+++ b/feature/interview-trainer/impl/src/main/res/values/strings.xml
@@ -19,4 +19,16 @@
Завершить
Назад
Далее
+ Результаты
+
+ Статистика пройденных вопросов собеседования
+ Прогресс обучения по навыкам
+ Посмотреть статистику
+ Список пройденных вопросов собеседования
+ пройдено
+ Всего
+ Новые
+ В процессе
+ Изучено
+
\ No newline at end of file
From d67071c80ae4abbc1840c1f4550e36731cdfe078 Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Mon, 30 Mar 2026 11:32:15 +0300
Subject: [PATCH 15/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-60:=20=D0=A2=D1=80=D0=B5=D1=82=D0=B8=D0=B9=20?=
=?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF=20InterviewQuizScreen.=20=D0=9B=D0=BE?=
=?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B0=20+=20=D0=A2=D0=B5=D1=81=D1=82=D1=8B?=
=?UTF-8?q?=20(#132)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-60: изменены getScreenState и динамическое превью. В VM удалены мок данные
* ANDR-60: добавление модели вопроса с ответом и сущности результата квиза
* ANDR-60: определены ивенты и команды
* ANDR-60: доработка VM, ScreenMapper и нормальная работа динамического превью
* ANDR-60: написание теста на ScreenMapper. Обработка исключения в VM
* ANDR-60: удаление недоступного для реализации функционала
* ANDR-60: улучшение оптимизации рекомпозиций. Небольшие фиксы
---
.../presentation/InterviewQuizCommand.kt | 6 +-
.../presentation/InterviewQuizEvent.kt | 14 +-
.../presentation/InterviewQuizResult.kt | 10 +
.../presentation/InterviewQuizScreenMapper.kt | 17 +-
.../presentation/InterviewQuizState.kt | 15 +-
.../presentation/InterviewQuizViewModel.kt | 193 ++++++++++++++----
.../presentation/VoQuestionWithAnswer.kt | 16 ++
.../interviewQuiz/ui/InterviewQuizScreen.kt | 121 +++++++++--
.../test/InterviewQuizScreenMapperTest.kt | 185 +++++++++++++++++
9 files changed, 513 insertions(+), 64 deletions(-)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizResult.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
create mode 100644 feature/interview-trainer/impl/src/test/java/test/InterviewQuizScreenMapperTest.kt
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
index 2fca28c0..46383131 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizCommand.kt
@@ -2,5 +2,9 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
sealed interface InterviewQuizCommand {
- data object ToDo : InterviewQuizCommand
+ data class NavigateToInterviewQuizResultScreen(
+ val questionsWithAnswersList: List
+ ) : InterviewQuizCommand
+
+ data object NavigateBack : InterviewQuizCommand
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
index 63fdf560..96baed1c 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizEvent.kt
@@ -2,5 +2,17 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
sealed interface InterviewQuizEvent {
- data object ToDo : InterviewQuizEvent
+ data object OnShowResultClick : InterviewQuizEvent
+
+ data object OnKnownAnswerClick : InterviewQuizEvent
+
+ data object OnUnknownAnswerClick : InterviewQuizEvent
+
+ data object OnNextQuestionClick : InterviewQuizEvent
+
+ data object OnPreviousQuestionClick : InterviewQuizEvent
+
+ data object OnShowHideAnswerClick : InterviewQuizEvent
+
+ data object OnBackClick : InterviewQuizEvent
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizResult.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizResult.kt
new file mode 100644
index 00000000..d69e21c4
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizResult.kt
@@ -0,0 +1,10 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+sealed interface InterviewQuizResult {
+
+ data class NavigateToInterviewQuizResultScreen(
+ val questionsWithAnswersList: List
+ ) : InterviewQuizResult
+
+ data object NavigateBack : InterviewQuizResult
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
index 8681cff5..fb0561d0 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizScreenMapper.kt
@@ -1,17 +1,24 @@
package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
-import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
+import kotlinx.collections.immutable.toPersistentList
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestion
class InterviewQuizScreenMapper {
fun getScreenState(
- questions: PersistentList,
+ domainQuestions: List,
questionIndex: Int,
isAnswerVisible: Boolean,
answers: PersistentMap,
selectedAnswer: InterviewQuizState.Loaded.QuizAnswer
): InterviewQuizState {
+ val questions = domainQuestions.map {
+ InterviewQuizState.Loaded.VoQuestion(it.id, it.title, it.shortAnswer)
+ }.toPersistentList()
+
val canGoNext = answers.containsKey(questions[questionIndex].id) &&
questionIndex != questions.lastIndex
@@ -21,9 +28,10 @@ class InterviewQuizScreenMapper {
val questionsCount = questions.size
- val isLastQuestion = questionIndex != questions.lastIndex
+ val isLastQuestion = questionIndex == questions.lastIndex
return InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
questions = questions,
questionsCount = questionsCount,
questionIndex = questionIndex,
@@ -36,4 +44,7 @@ class InterviewQuizScreenMapper {
isLastQuestion = isLastQuestion
)
}
+
+ fun getScreenState(e: Throwable): InterviewQuizState =
+ InterviewQuizState.Error(e)
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
index 4e2b89f2..f2015d10 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizState.kt
@@ -3,14 +3,22 @@ package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
sealed interface InterviewQuizState {
+ val titleTopAppBar: TextOrResource
+
/** Изначальное состояние */
- data object Loading : InterviewQuizState
+ data object Loading : InterviewQuizState {
+
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
@Immutable
data class Loaded(
+ override val titleTopAppBar: TextOrResource,
val questions: PersistentList,
val questionsCount: Int,
val questionIndex: Int,
@@ -33,5 +41,8 @@ sealed interface InterviewQuizState {
)
}
- data class Error(val throwable: Throwable) : InterviewQuizState
+ data class Error(val throwable: Throwable) : InterviewQuizState {
+
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
index f23c30ca..af88a583 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
@@ -1,46 +1,63 @@
package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
-import kotlinx.collections.immutable.PersistentList
+import androidx.lifecycle.SavedStateHandle
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
-import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import ru.yeahub.core_utils.BaseViewModel
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.GetQuestionsListUseCase
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.QuestionsRequest
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.QuizAnswer
-import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState.Loaded.VoQuestion
open class InterviewQuizViewModel(
- private val screenMapper: InterviewQuizScreenMapper
+ savedStateHandle: SavedStateHandle,
+ private val screenMapper: InterviewQuizScreenMapper,
+ private val getQuestionsListUseCase: GetQuestionsListUseCase
) : BaseViewModel() {
- // Вопросы для превью. Временно
- private val previewQuestions by lazy {
- previewQuestions()
+ private val specializationId: Int =
+ requireNotNull(savedStateHandle.get("specializationId")).toInt()
+
+ private val questionsCount: Int =
+ requireNotNull(savedStateHandle.get("questionsCount")).toInt()
+
+ private val questionsDeferred = viewModelScopeSafe.async(Dispatchers.IO) {
+ val request = QuestionsRequest(questionsCount, specializationId)
+ getQuestionsListUseCase(request)
}
private val userInputState = MutableStateFlow(
UserInput(
+ questionIndex = FIRST_QUESTION_INDEX,
isAnswerVisible = false,
answers = persistentMapOf(),
selectedAnswer = QuizAnswer.NONE
)
)
- val screenState = userInputState
- .map { userInput ->
- screenMapper.getScreenState(
- questions = previewQuestions,
- questionIndex = FIRST_QUESTION_INDEX,
- isAnswerVisible = userInput.isAnswerVisible,
- answers = userInput.answers,
- selectedAnswer = userInput.selectedAnswer
- )
- }.stateIn(
+ val screenState = userInputState.map { userInput ->
+ val response = questionsDeferred.await()
+
+ screenMapper.getScreenState(
+ domainQuestions = response.questions,
+ questionIndex = userInput.questionIndex,
+ isAnswerVisible = userInput.isAnswerVisible,
+ answers = userInput.answers,
+ selectedAnswer = userInput.selectedAnswer
+ )
+ }.catch { e ->
+ screenMapper.getScreenState(e)
+ }.stateIn(
scope = viewModelScopeSafe,
started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
initialValue = InterviewQuizState.Loading
@@ -51,32 +68,138 @@ open class InterviewQuizViewModel(
fun onEvent(event: InterviewQuizEvent) {
when (event) {
- InterviewQuizEvent.ToDo -> { /* TODO */ }
+ InterviewQuizEvent.OnBackClick -> onBackClick()
+ InterviewQuizEvent.OnKnownAnswerClick -> onKnownAnswerClick()
+ InterviewQuizEvent.OnUnknownAnswerClick -> onUnknownAnswerClick()
+ InterviewQuizEvent.OnShowResultClick -> onFinishInterviewClick()
+ InterviewQuizEvent.OnNextQuestionClick -> onNextQuestionClick()
+ InterviewQuizEvent.OnPreviousQuestionClick -> onPreviousQuestionClick()
+ InterviewQuizEvent.OnShowHideAnswerClick -> onShowHideAnswerClick()
}
}
- /** Создание списка вопросов для тестирования превью */
- @Suppress("MagicNumber")
- private fun previewQuestions(): PersistentList {
- val shortAnswer = "Виртуальный DOM (VDOM) — это легковесное " +
- "представление реального DOM в памяти, которое используется в " +
- "JavaScript-библиотеках, таких как React и Vue, " +
- "для повышения производительности веб-приложений."
-
- val base = VoQuestion(
- id = 0,
- title = "Что такое Virtual DOM, и как он работает?",
- shortAnswer = shortAnswer
- )
- val questions = mutableListOf()
- repeat(10) { index ->
- questions.add(base.copy(id = index.toLong()))
+ private fun onBackClick() {
+ viewModelScopeSafe.launch(Dispatchers.IO) {
+ _commands.emit(InterviewQuizCommand.NavigateBack)
+ }
+ }
+
+ private fun onKnownAnswerClick() {
+ val state = screenState.value as? InterviewQuizState.Loaded ?: return
+ val currentQuestionId = state.question.id
+
+ userInputState.update { currentInputState ->
+ val newAnswer = QuizAnswer.KNOWN
+
+ currentInputState.copy(
+ selectedAnswer = newAnswer,
+ answers = currentInputState.answers.put(currentQuestionId, newAnswer)
+ )
+ }
+ }
+
+ private fun onUnknownAnswerClick() {
+ val state = screenState.value as? InterviewQuizState.Loaded ?: return
+ val currentQuestionId = state.question.id
+
+ userInputState.update { currentInputState ->
+ val newAnswer = QuizAnswer.UNKNOWN
+
+ currentInputState.copy(
+ selectedAnswer = newAnswer,
+ answers = currentInputState.answers.put(currentQuestionId, newAnswer)
+ )
+ }
+ }
+
+ private fun onFinishInterviewClick() {
+ viewModelScopeSafe.launch(Dispatchers.IO) {
+ val resultList = buildQuestionsWithAnswers()
+ if (resultList.isEmpty()) return@launch
+
+ _commands.emit(
+ InterviewQuizCommand.NavigateToInterviewQuizResultScreen(
+ questionsWithAnswersList = resultList
+ )
+ )
}
+ }
+
+ private fun onNextQuestionClick() {
+ val state = screenState.value as? InterviewQuizState.Loaded ?: return
+
+ userInputState.update { currentInputState ->
+ if (!state.canGoNext) return@update currentInputState
+
+ val newQuestionIndex = currentInputState.questionIndex + 1
+ val newQuestion = state.questions[newQuestionIndex]
+ val newSelectedAnswer = currentInputState.answers[newQuestion.id] ?: QuizAnswer.NONE
+
+ currentInputState.copy(
+ questionIndex = newQuestionIndex,
+ isAnswerVisible = false,
+ selectedAnswer = newSelectedAnswer
+ )
+ }
+ }
+
+ private fun onPreviousQuestionClick() {
+ val state = screenState.value as? InterviewQuizState.Loaded ?: return
- return questions.toPersistentList()
+ userInputState.update { currentInputState ->
+ if (!state.canGoPrev) return@update currentInputState
+
+ val newQuestionIndex = currentInputState.questionIndex - 1
+ val newQuestion = state.questions[newQuestionIndex]
+ val newSelectedAnswer = currentInputState.answers[newQuestion.id] ?: QuizAnswer.NONE
+
+ currentInputState.copy(
+ questionIndex = newQuestionIndex,
+ isAnswerVisible = false,
+ selectedAnswer = newSelectedAnswer
+ )
+ }
+ }
+
+ private fun onShowHideAnswerClick() {
+ userInputState.update { currentInputState ->
+ currentInputState.copy(
+ isAnswerVisible = !currentInputState.isAnswerVisible
+ )
+ }
+ }
+
+ private fun buildQuestionsWithAnswers(): List {
+ val state = screenState.value as? InterviewQuizState.Loaded
+ ?: return emptyList()
+
+ return state.questions.mapNotNull { question ->
+ when (state.answers[question.id]) {
+ QuizAnswer.KNOWN -> {
+ VoQuestionWithAnswer(
+ id = question.id,
+ title = question.title,
+ shortAnswer = question.shortAnswer,
+ userAnswer = QuizAnswerResult.KNOWN
+ )
+ }
+
+ QuizAnswer.UNKNOWN -> {
+ VoQuestionWithAnswer(
+ id = question.id,
+ title = question.title,
+ shortAnswer = question.shortAnswer,
+ userAnswer = QuizAnswerResult.UNKNOWN
+ )
+ }
+
+ QuizAnswer.NONE, null -> null
+ }
+ }
}
private data class UserInput(
+ val questionIndex: Int,
val isAnswerVisible: Boolean,
val answers: PersistentMap,
val selectedAnswer: QuizAnswer
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
new file mode 100644
index 00000000..cb3e3002
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
@@ -0,0 +1,16 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class VoQuestionWithAnswer(
+ val id: Long,
+ val title: String,
+ val shortAnswer: String,
+ val userAnswer: QuizAnswerResult
+)
+
+enum class QuizAnswerResult {
+ KNOWN,
+ UNKNOWN
+}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
index 72cb22d5..bde28292 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
@@ -27,7 +27,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@@ -44,9 +44,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import org.koin.androidx.compose.koinViewModel
import ru.yeahub.core_ui.component.ErrorScreen
import ru.yeahub.core_ui.component.KnownAnswerButton
import ru.yeahub.core_ui.component.PrimaryButton
@@ -57,8 +62,15 @@ import ru.yeahub.core_ui.component.YeahubButtonDefaults
import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.Theme
import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.core_utils.common.observe
import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestion
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestionsListResponse
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.GetQuestionsListUseCase
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.QuestionsRequest
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizCommand
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizEvent
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizResult
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizScreenMapper
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizState
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizViewModel
@@ -70,40 +82,56 @@ private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
private val FIGMA_CARD_ELEVATION = 4.dp
private val FIGMA_RADIUS = 12.dp
+@Composable
+fun InterviewQuizScreen(onResult: (InterviewQuizResult) -> Unit) {
+ val viewModel: InterviewQuizViewModel = koinViewModel()
+
+ val screenState = viewModel.screenState.collectAsStateWithLifecycle()
+
+ HandleCommand(
+ commandFlow = viewModel.commands,
+ onResult = onResult
+ )
+
+ ScreenUI(
+ state = screenState,
+ onEvent = viewModel::onEvent
+ )
+}
+
@Composable
private fun ScreenUI(
- headerText: TextOrResource,
- state: InterviewQuizState,
+ state: State,
onEvent: (InterviewQuizEvent) -> Unit
) {
Scaffold(
containerColor = Theme.colors.black10,
topBar = {
TopAppBarWithBottomBorder(
- title = headerText,
- onBackClick = { TODO("onBackClick don't implemented") }
+ title = state.value.titleTopAppBar,
+ onBackClick = { onEvent(InterviewQuizEvent.OnBackClick) }
)
}
) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
- when (state) {
+ when (val currentState = state.value) {
is InterviewQuizState.Loaded -> BaseQuizScreen(
- state = state,
- onPreviousClick = { /* TODO */ },
- onNextClick = { /* TODO */ },
- onUnknownClick = { /* TODO */ },
- onKnownClick = { /* TODO */ },
- onShowAnswerClick = { /* TODO */ },
- onResultClick = { /* TODO */ }
+ state = currentState,
+ onPreviousClick = { onEvent(InterviewQuizEvent.OnPreviousQuestionClick) },
+ onNextClick = { onEvent(InterviewQuizEvent.OnNextQuestionClick) },
+ onUnknownClick = { onEvent(InterviewQuizEvent.OnUnknownAnswerClick) },
+ onKnownClick = { onEvent(InterviewQuizEvent.OnKnownAnswerClick) },
+ onShowAnswerClick = { onEvent(InterviewQuizEvent.OnShowHideAnswerClick) },
+ onResultClick = { onEvent(InterviewQuizEvent.OnShowResultClick) }
)
is InterviewQuizState.Error -> ErrorScreen(
- error = state.throwable.localizedMessage,
+ error = currentState.throwable.localizedMessage,
errorText = TextOrResource.Resource(R.string.error_screen_text),
titleText = TextOrResource.Resource(R.string.title_error_screen_text),
backText = TextOrResource.Resource(R.string.back_error_screen_text),
unknownErrorText = TextOrResource.Resource(R.string.unknown_error_screen_text),
- onBack = { TODO() }
+ onBack = { onEvent(InterviewQuizEvent.OnBackClick) }
)
InterviewQuizState.Loading -> InterviewQuizLoading()
@@ -112,6 +140,24 @@ private fun ScreenUI(
}
}
+@Composable
+private fun HandleCommand(
+ commandFlow: Flow,
+ onResult: (InterviewQuizResult) -> Unit,
+) {
+ commandFlow.observe { command ->
+ when (command) {
+ is InterviewQuizCommand.NavigateBack -> onResult(InterviewQuizResult.NavigateBack)
+
+ is InterviewQuizCommand.NavigateToInterviewQuizResultScreen -> onResult(
+ InterviewQuizResult.NavigateToInterviewQuizResultScreen(
+ questionsWithAnswersList = command.questionsWithAnswersList
+ )
+ )
+ }
+ }
+}
+
@Composable
private fun BaseQuizScreen(
state: InterviewQuizState.Loaded,
@@ -191,7 +237,6 @@ private fun QuestionCard(
onShowAnswerClick: () -> Unit,
onResultClick: () -> Unit
) {
- // TODO: нет фичи профиля
var isFavorite by rememberSaveable { mutableStateOf(false) }
val favoriteIcon: Painter =
@@ -242,7 +287,7 @@ private fun QuestionCard(
style = Theme.typography.body3Strong
)
FilledIconButton(
- onClick = { isFavorite = !isFavorite },
+ onClick = { },
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(FIGMA_RADIUS),
colors = IconButtonDefaults.filledIconButtonColors(
@@ -430,6 +475,7 @@ private val answers = persistentMapOf(
class QuizScreenStateParamProvider : PreviewParameterProvider {
override val values: Sequence = sequenceOf(
InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
questions = questions,
questionsCount = questions.size,
questionIndex = 0,
@@ -442,6 +488,7 @@ class QuizScreenStateParamProvider : PreviewParameterProvider,
+ val questionIndex: Int,
+ val isAnswerVisible: Boolean,
+ val answers: PersistentMap,
+ val selectedAnswer: InterviewQuizState.Loaded.QuizAnswer,
+ val expectedResult: InterviewQuizState.Loaded
+ )
+
+ class ArgumentsProvider :
+ TestArgumentsProvider() {
+
+ override fun testCases(): List = listOf(
+ InterviewQuizScreenMapperTestCase(
+ domainQuestions = ScreenMapperExampleDataClasses.defaultDomainQuestions,
+ questionIndex = 0,
+ isAnswerVisible = false,
+ answers = persistentMapOf(),
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.NONE,
+ expectedResult = InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ questions = ScreenMapperExampleDataClasses.defaultVoQuestions,
+ questionsCount = 3,
+ questionIndex = 0,
+ question = ScreenMapperExampleDataClasses.defaultVoQuestions[0],
+ isAnswerVisible = false,
+ answers = persistentMapOf(),
+ canGoPrev = false,
+ canGoNext = false,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.NONE,
+ isLastQuestion = false
+ )
+ ),
+ InterviewQuizScreenMapperTestCase(
+ domainQuestions = ScreenMapperExampleDataClasses.defaultDomainQuestions,
+ questionIndex = 0,
+ isAnswerVisible = true,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ ),
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ expectedResult = InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ questions = ScreenMapperExampleDataClasses.defaultVoQuestions,
+ questionsCount = 3,
+ questionIndex = 0,
+ question = ScreenMapperExampleDataClasses.defaultVoQuestions[0],
+ isAnswerVisible = true,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ ),
+ canGoPrev = false,
+ canGoNext = true,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ isLastQuestion = false
+ )
+ ),
+ InterviewQuizScreenMapperTestCase(
+ domainQuestions = ScreenMapperExampleDataClasses.defaultDomainQuestions,
+ questionIndex = 1,
+ isAnswerVisible = false,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ 2L to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN
+ ),
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ expectedResult = InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ questions = ScreenMapperExampleDataClasses.defaultVoQuestions,
+ questionsCount = 3,
+ questionIndex = 1,
+ question = ScreenMapperExampleDataClasses.defaultVoQuestions[1],
+ isAnswerVisible = false,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ 2L to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN
+ ),
+ canGoPrev = true,
+ canGoNext = true,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ isLastQuestion = false
+ )
+ ),
+ InterviewQuizScreenMapperTestCase(
+ domainQuestions = ScreenMapperExampleDataClasses.defaultDomainQuestions,
+ questionIndex = 2,
+ isAnswerVisible = true,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ 2L to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ 3L to InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ ),
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ expectedResult = InterviewQuizState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ questions = ScreenMapperExampleDataClasses.defaultVoQuestions,
+ questionsCount = 3,
+ questionIndex = 2,
+ question = ScreenMapperExampleDataClasses.defaultVoQuestions[2],
+ isAnswerVisible = true,
+ answers = persistentMapOf(
+ 1L to InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ 2L to InterviewQuizState.Loaded.QuizAnswer.UNKNOWN,
+ 3L to InterviewQuizState.Loaded.QuizAnswer.KNOWN
+ ),
+ canGoPrev = true,
+ canGoNext = false,
+ selectedAnswer = InterviewQuizState.Loaded.QuizAnswer.KNOWN,
+ isLastQuestion = true
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
From 228b7cc35cb8f7584d28ea5a557e479c8ce9c30c Mon Sep 17 00:00:00 2001
From: xMODDIIx <91845532+xMODDIIx@users.noreply.github.com>
Date: Mon, 30 Mar 2026 12:03:52 +0300
Subject: [PATCH 16/28] Feature/andr 95 (#131)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-95: вынос общих полей ввода и логики валидации в core модули
* ANDR-95: Обновил core/ui/build.gradle.kts: Добавил implementation(libs.androidx.icons)
* ANDR-95: улучшена функция для максимальной переиспользуемости. Так же улучшена Preview часть
* ANDR-95: переименовал функции понятным для всех неймингом, исправил ошибки ktlint
* ANDR-95: рефакторинг TextField + реализация SearchTextField
* ANDR-95: удаление старого после рефакторинга
---
core/ui/build.gradle.kts | 1 +
.../ru/yeahub/core_ui/component/TextField.kt | 566 ++++++++++++++++++
.../textInput/SuggestionsViewModel.kt | 22 -
.../core_ui/component/textInput/TextInput.kt | 472 ---------------
.../core_utils/validation/EmailValidator.kt | 10 +
.../validation/PasswordValidationError.kt | 11 +
.../validation/PasswordValidator.kt | 28 +
7 files changed, 616 insertions(+), 494 deletions(-)
create mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
delete mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/SuggestionsViewModel.kt
delete mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/TextInput.kt
create mode 100644 core/utils/src/main/java/ru/yeahub/core_utils/validation/EmailValidator.kt
create mode 100644 core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidationError.kt
create mode 100644 core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidator.kt
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index eb8470a0..87d774ec 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.material3)
implementation(libs.androidx.runtime.android)
+ implementation(libs.androidx.icons)
implementation(libs.coil.compose)
implementation(libs.jsoup)
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
new file mode 100644
index 00000000..2f1b8de4
--- /dev/null
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
@@ -0,0 +1,566 @@
+package ru.yeahub.core_ui.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import ru.yeahub.core_ui.theme.Theme
+import ru.yeahub.core_ui.theme.YeaHubTheme
+import ru.yeahub.core_utils.common.TextOrResource
+
+/**
+ * Набор цветов для текстовых полей YeaHub.
+ *
+ * Используйте [YeahubTextFieldDefaults.colors] для создания экземпляра
+ * с дефолтными значениями из темы, переопределяя только нужные цвета.
+ */
+@Immutable
+data class YeahubTextFieldColors(
+ val focusedTextColor: Color,
+ val unfocusedTextColor: Color,
+ val disabledTextColor: Color,
+ val focusedBorderColor: Color,
+ val unfocusedBorderColor: Color,
+ val disabledBorderColor: Color,
+ val errorBorderColor: Color,
+ val cursorColor: Color,
+ val focusedLeadingIconColor: Color,
+ val unfocusedLeadingIconColor: Color,
+ val focusedTrailingIconColor: Color,
+ val unfocusedTrailingIconColor: Color,
+ val titleColor: Color,
+ val placeholderColor: Color,
+ val errorColor: Color,
+)
+
+object YeahubTextFieldDefaults {
+ val Shape: Shape = RoundedCornerShape(12.dp)
+
+ @Composable
+ fun colors(
+ focusedTextColor: Color = Theme.colors.black900,
+ unfocusedTextColor: Color = Theme.colors.black900,
+ disabledTextColor: Color = Theme.colors.black500,
+ focusedBorderColor: Color = Theme.colors.purple700,
+ unfocusedBorderColor: Color = Theme.colors.black100,
+ disabledBorderColor: Color = Theme.colors.black100,
+ errorBorderColor: Color = Theme.colors.red700,
+ cursorColor: Color = Theme.colors.purple700,
+ focusedLeadingIconColor: Color = Theme.colors.black900,
+ unfocusedLeadingIconColor: Color = Theme.colors.black500,
+ focusedTrailingIconColor: Color = Theme.colors.black900,
+ unfocusedTrailingIconColor: Color = Theme.colors.black500,
+ titleColor: Color = Theme.colors.black900,
+ placeholderColor: Color = Theme.colors.black500,
+ errorColor: Color = Theme.colors.red700,
+ ): YeahubTextFieldColors = YeahubTextFieldColors(
+ focusedTextColor = focusedTextColor,
+ unfocusedTextColor = unfocusedTextColor,
+ disabledTextColor = disabledTextColor,
+ focusedBorderColor = focusedBorderColor,
+ unfocusedBorderColor = unfocusedBorderColor,
+ disabledBorderColor = disabledBorderColor,
+ errorBorderColor = errorBorderColor,
+ cursorColor = cursorColor,
+ focusedLeadingIconColor = focusedLeadingIconColor,
+ unfocusedLeadingIconColor = unfocusedLeadingIconColor,
+ focusedTrailingIconColor = focusedTrailingIconColor,
+ unfocusedTrailingIconColor = unfocusedTrailingIconColor,
+ titleColor = titleColor,
+ placeholderColor = placeholderColor,
+ errorColor = errorColor,
+ )
+}
+
+// 1. БАЗА (дизайн YeaHub)
+
+@Composable
+internal fun CoreTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ placeholder: String? = null,
+ error: TextOrResource? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ singleLine: Boolean = true,
+ enabled: Boolean = true,
+ colors: YeahubTextFieldColors = YeahubTextFieldDefaults.colors(),
+ shape: Shape = YeahubTextFieldDefaults.Shape,
+ onFocusChanged: (Boolean) -> Unit = {},
+) {
+ val errorText = error?.let { error ->
+ when (error) {
+ is TextOrResource.Resource -> stringResource(error.resource)
+ is TextOrResource.Text -> error.text
+ }
+ }
+
+ val textFieldValueState = remember { mutableStateOf(TextFieldValue(text = value)) }
+ val textFieldValue = if (textFieldValueState.value.text == value) {
+ textFieldValueState.value
+ } else {
+ TextFieldValue(text = value, selection = TextRange(value.length))
+ }
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ if (title != null) {
+ Text(
+ text = title,
+ style = Theme.typography.body2Accent,
+ color = colors.titleColor
+ )
+ }
+
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged { onFocusChanged(it.isFocused) }
+ .then(
+ if (errorText != null) {
+ Modifier.semantics { error(errorText) }
+ } else {
+ Modifier
+ }
+ ),
+ value = textFieldValue,
+ onValueChange = { newTextFieldValue ->
+ textFieldValueState.value = newTextFieldValue
+ if (newTextFieldValue.text != value) {
+ onValueChange(newTextFieldValue.text)
+ }
+ },
+ enabled = enabled,
+ isError = error != null,
+ visualTransformation = visualTransformation,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ placeholder = placeholder?.let {
+ { Text(text = it, style = Theme.typography.body2, color = colors.placeholderColor) }
+ },
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ singleLine = singleLine,
+ shape = shape,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedTextColor = colors.focusedTextColor,
+ unfocusedTextColor = colors.unfocusedTextColor,
+ disabledTextColor = colors.disabledTextColor,
+ focusedBorderColor = colors.focusedBorderColor,
+ unfocusedBorderColor = colors.unfocusedBorderColor,
+ disabledBorderColor = colors.disabledBorderColor,
+ errorBorderColor = colors.errorBorderColor,
+ cursorColor = colors.cursorColor,
+ focusedLeadingIconColor = colors.focusedLeadingIconColor,
+ unfocusedLeadingIconColor = colors.unfocusedLeadingIconColor,
+ focusedTrailingIconColor = colors.focusedTrailingIconColor,
+ unfocusedTrailingIconColor = colors.unfocusedTrailingIconColor,
+ )
+ )
+
+ AnimatedVisibility(
+ visible = errorText != null,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut(),
+ ) {
+ if (errorText != null) {
+ Text(
+ text = errorText,
+ color = colors.errorColor,
+ style = Theme.typography.body1
+ )
+ }
+ }
+ }
+}
+
+// 2. Основное текстовое поле дизайн-системы YeaHub
+
+/**
+ * @param value текущее значение поля.
+ * @param onValueChange колбэк при изменении текста.
+ * @param modifier модификатор для корневого контейнера.
+ * @param title заголовок над полем (опционально).
+ * @param placeholder текст-подсказка (опционально).
+ * @param error текст ошибки; если не null — поле подсвечивается красным
+ * и текст ошибки отображается под полем с анимацией.
+ * @param leadingIcon иконка слева (опционально).
+ * @param leadingIconContentDescription описание leading-иконки для Accessibility / TalkBack.
+ * @param trailingIcon иконка справа (опционально).
+ * @param trailingIconContentDescription описание trailing-иконки для Accessibility / TalkBack.
+ * @param onTrailingIconClick колбэк нажатия на trailing-иконку.
+ * Если null — иконка отображается без кликабельности.
+ * @param visualTransformation трансформация отображения (например, [PasswordVisualTransformation]).
+ * @param keyboardOptions опции клавиатуры.
+ * @param keyboardActions действия клавиатуры.
+ * @param singleLine однострочное поле.
+ * @param enabled доступность для ввода.
+ * @param colors набор цветов. Используйте [YeahubTextFieldDefaults.colors]
+ * для точечного переопределения.
+ * @param onFocusChanged колбэк при изменении фокуса.
+ */
+@Composable
+fun PrimaryTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ placeholder: String? = null,
+ error: TextOrResource? = null,
+ leadingIcon: Painter? = null,
+ leadingIconContentDescription: String? = null,
+ trailingIcon: Painter? = null,
+ trailingIconContentDescription: String? = null,
+ onTrailingIconClick: (() -> Unit)? = null,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ singleLine: Boolean = true,
+ enabled: Boolean = true,
+ colors: YeahubTextFieldColors = YeahubTextFieldDefaults.colors(),
+ onFocusChanged: (Boolean) -> Unit = {},
+) {
+ CoreTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ title = title,
+ placeholder = placeholder,
+ error = error,
+ visualTransformation = visualTransformation,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ singleLine = singleLine,
+ enabled = enabled,
+ colors = colors,
+ onFocusChanged = onFocusChanged,
+ leadingIcon = leadingIcon?.let { icon ->
+ {
+ Icon(
+ painter = icon,
+ contentDescription = leadingIconContentDescription,
+ tint = colors.unfocusedLeadingIconColor
+ )
+ }
+ },
+ trailingIcon = trailingIcon?.let { icon ->
+ {
+ if (onTrailingIconClick != null) {
+ IconButton(onClick = onTrailingIconClick, enabled = enabled) {
+ Icon(
+ painter = icon,
+ contentDescription = trailingIconContentDescription,
+ tint = colors.unfocusedTrailingIconColor
+ )
+ }
+ } else {
+ Icon(
+ painter = icon,
+ contentDescription = trailingIconContentDescription,
+ tint = colors.unfocusedTrailingIconColor
+ )
+ }
+ }
+ }
+ )
+}
+
+// 3. ОБЕРТКА ДЛЯ ПОИСКА С САДЖЕСТАМИ
+
+/**
+ * @param value текущее значение поля.
+ * @param onValueChange колбэк при изменении текста.
+ * @param modifier модификатор для корневого контейнера.
+ * @param title заголовок над полем (опционально).
+ * @param placeholder текст-подсказка.
+ * @param suggestions список предложений для отображения.
+ * @param onSuggestionClick колбэк при выборе предложения.
+ * @param isExpanded управление видимостью списка. По умолчанию — виден, если [suggestions] не пуст.
+ * @param enabled доступность для ввода.
+ * @param colors набор цветов. Используйте [YeahubTextFieldDefaults.colors]
+ * для точечного переопределения.
+ * @param onSearch колбэк при нажатии кнопки "Поиск" на клавиатуре.
+ */
+@Composable
+fun SearchTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ placeholder: String,
+ suggestions: List,
+ onSuggestionClick: (String) -> Unit,
+ isExpanded: Boolean = suggestions.isNotEmpty(),
+ enabled: Boolean = true,
+ colors: YeahubTextFieldColors = YeahubTextFieldDefaults.colors(),
+ onSearch: () -> Unit = {}
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+
+ val filteredSuggestions = suggestions.filter { !it.equals(value, ignoreCase = true) }
+ val showSuggestions = isExpanded && filteredSuggestions.isNotEmpty()
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ CoreTextField(
+ value = value,
+ title = title,
+ onValueChange = onValueChange,
+ placeholder = placeholder,
+ enabled = enabled,
+ colors = colors,
+ leadingIcon = {
+ Icon(
+ painter = rememberVectorPainter(Icons.Default.Search),
+ contentDescription = null,
+ tint = colors.unfocusedLeadingIconColor
+ )
+ },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ onSearch()
+ keyboardController?.hide()
+ focusManager.clearFocus(force = true)
+ }
+ )
+ )
+
+ AnimatedVisibility(visible = showSuggestions) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp),
+ shape = MaterialTheme.shapes.medium,
+ color = Theme.colors.white900,
+ border = BorderStroke(1.dp, Theme.colors.black50),
+ shadowElevation = 4.dp
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 200.dp)
+ ) {
+ items(filteredSuggestions) { suggestion ->
+ Text(
+ text = suggestion,
+ style = Theme.typography.body3,
+ color = Theme.colors.black900,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ onSuggestionClick(suggestion)
+ keyboardController?.hide()
+ focusManager.clearFocus(force = true)
+ }
+ .padding(16.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+// Previews
+
+internal data class TextFieldState(
+ val name: String,
+ val value: String,
+ val title: String? = null,
+ val placeholder: String? = null,
+ val error: TextOrResource? = null,
+ val isPassword: Boolean = false,
+ val hasAction: Boolean = false,
+ val enabled: Boolean = true
+)
+
+internal class TextFieldProvider : PreviewParameterProvider {
+ override val values = sequenceOf(
+ TextFieldState("1. Default", "", "Email", "example@mail.com"),
+ TextFieldState("2. Filled + Action", "Текст", "Поиск", hasAction = true),
+ TextFieldState("3. Password", "password123", "Пароль", isPassword = true),
+ TextFieldState("4. Error", "123", "Логин", error = TextOrResource.Text("Ошибка валидации")),
+ TextFieldState("5. Disabled", "Заблокировано", enabled = false),
+ TextFieldState("6. No Title", "Просто поле")
+ )
+}
+
+// Статичные превью для всех вариантов дизайна
+@Preview(showBackground = true)
+@Composable
+internal fun PrimaryTextFieldStaticPreview(
+ @PreviewParameter(TextFieldProvider::class) state: TextFieldState
+) {
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.white900)
+ .padding(16.dp)
+ ) {
+ val visualTransformation =
+ if (state.isPassword) PasswordVisualTransformation() else VisualTransformation.None
+ val actionIcon = when {
+ state.isPassword -> rememberVectorPainter(Icons.Filled.Visibility)
+ state.hasAction -> rememberVectorPainter(Icons.Filled.Clear)
+ else -> null
+ }
+ PrimaryTextField(
+ title = state.title,
+ placeholder = state.placeholder,
+ value = state.value,
+ onValueChange = {},
+ error = state.error,
+ enabled = state.enabled,
+ visualTransformation = visualTransformation,
+ trailingIcon = actionIcon,
+ trailingIconContentDescription = when {
+ state.isPassword -> "Показать пароль"
+ state.hasAction -> "Очистить"
+ else -> null
+ },
+ onTrailingIconClick = if (actionIcon != null) {
+ {}
+ } else {
+ null
+ }
+ )
+ }
+ }
+}
+
+// Интерактивное превью для PrimaryTextField (с показом/скрытием пароля)
+@Preview(name = "Интерактивный пароль", showBackground = true)
+@Composable
+internal fun PrimaryTextFieldInteractivePreview() {
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.white900)
+ .padding(16.dp)
+ ) {
+ var text by remember { mutableStateOf("") }
+ var isPasswordVisible by remember { mutableStateOf(false) }
+
+ PrimaryTextField(
+ title = "Интерактивный пароль",
+ placeholder = "Введите пароль...",
+ value = text,
+ onValueChange = { text = it },
+ visualTransformation = if (isPasswordVisible) {
+ VisualTransformation.None
+ } else {
+ PasswordVisualTransformation()
+ },
+ trailingIcon = rememberVectorPainter(
+ if (isPasswordVisible) {
+ Icons.Filled.VisibilityOff
+ } else {
+ Icons.Filled.Visibility
+ }
+ ),
+ trailingIconContentDescription = if (isPasswordVisible) {
+ "Скрыть пароль"
+ } else {
+ "Показать пароль"
+ },
+ onTrailingIconClick = { isPasswordVisible = !isPasswordVisible }
+ )
+ }
+ }
+}
+
+// Интерактивное превью для поиска с реальной фильтрацией
+@Preview(name = "Интерактивный поиск с предложением", showBackground = true)
+@Composable
+internal fun SearchTextFieldInteractivePreview() {
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.white900)
+ .padding(16.dp)
+ ) {
+ var text by remember { mutableStateOf("") }
+
+ val allSuggestions =
+ listOf("Telephone", "Television", "Telescope", "Tablet", "Laptop")
+
+ val currentSuggestions = if (text.length > 1) {
+ allSuggestions.filter { it.contains(text, ignoreCase = true) }
+ } else {
+ emptyList()
+ }
+
+ SearchTextField(
+ value = text,
+ title = "Интерактивный поиск",
+ onValueChange = { text = it },
+ placeholder = "Начни вводить 'Tel'...",
+ suggestions = currentSuggestions,
+ onSuggestionClick = {
+ text = it
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/SuggestionsViewModel.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/SuggestionsViewModel.kt
deleted file mode 100644
index d11113c9..00000000
--- a/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/SuggestionsViewModel.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package ru.yeahub.core_ui.component.textInput
-
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.lifecycle.ViewModel
-
-open class SuggestionsViewModel() : ViewModel() {
-
- private val _text = mutableStateOf("")
- val text: State = _text
-
- private val _suggestions = mutableStateOf(emptyList())
- val suggestions: State> = _suggestions
-
- fun onTextChanged(newTextPreview: String) {
- _text.value = newTextPreview
- }
-
- fun onQueryChange(newTextPreview: String, suggestionsPreview: List) {
- _suggestions.value = suggestionsPreview.filter { it.startsWith(newTextPreview) }
- }
-}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/TextInput.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/TextInput.kt
deleted file mode 100644
index bfc8ab96..00000000
--- a/core/ui/src/main/java/ru/yeahub/core_ui/component/textInput/TextInput.kt
+++ /dev/null
@@ -1,472 +0,0 @@
-package ru.yeahub.core_ui.component.textInput
-
-import android.annotation.SuppressLint
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsFocusedAsState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
-import androidx.compose.material3.SearchBar
-import androidx.compose.material3.SearchBarDefaults
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.delay
-import ru.yeahub.core_ui.example.dynamicPreview.StandardScreenSizePreview
-import ru.yeahub.core_ui.example.staticPreview.StaticPreview
-import ru.yeahub.core_ui.theme.Theme
-import ru.yeahub.ui.R
-
-private sealed class TextInputState {
- data object Default : TextInputState()
- data object Focused : TextInputState()
- data object Error : TextInputState()
- data object Disabled : TextInputState()
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun TextInput(
- value: String,
- onValueChange: (String) -> Unit,
- modifier: Modifier = Modifier,
- label: String,
- isEnabled: Boolean = true,
- isError: Boolean = false,
- expanded: Boolean,
- onExpandedChange: (Boolean) -> Unit,
- suggestions: List = emptyList(),
- onQueryChanged: (String) -> Unit,
- colors: ColorsTextInputYeaHub = TextInputColorsDefaults.defaultColors(),
- shape: Shape = RoundedCornerShape(12.dp),
- contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding(),
- singleLine: Boolean = true,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-) {
- val isFocused by interactionSource.collectIsFocusedAsState()
-
- LaunchedEffect(value) {
- if (value.isNotEmpty()) {
- delay(300)
- onQueryChanged(value)
- onExpandedChange(true)
- } else {
- onExpandedChange(false)
- }
- }
- Box(
- modifier = modifier
- .width(300.dp)
- .height(300.dp),
- ) {
- SearchBar(
- inputField = {
- DefaultTextField(
- value = value,
- onValueChange = { newValue ->
- onValueChange(newValue)
- },
- modifier = Modifier,
- placeholder = label,
- isFocused = isFocused,
- isEnabled = isEnabled,
- isError = isError,
- onExpandedChange = onExpandedChange,
- colors = colors,
- shape = shape,
- singleLine = singleLine,
- interactionSource = interactionSource,
- )
- },
- expanded = expanded,
- onExpandedChange = onExpandedChange,
- modifier = Modifier
- .fillMaxWidth(),
- shape = shape,
- colors = SearchBarDefaults.colors(
- containerColor = Color.Transparent,
- dividerColor = Color.Transparent,
- ),
- ) {
- Surface(
- modifier = Modifier
- .fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- color = Theme.colors.white900,
- border = BorderStroke(1.dp, TextInputColorsDefaults.defaultsBorder()),
- ) {
- LazyColumn(
- modifier = Modifier.fillMaxWidth(),
- ) {
- items(suggestions) { suggestion ->
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- onValueChange(suggestion)
- onExpandedChange(false)
- }
- .padding(contentPadding),
- ) {
- Text(
- text = suggestion,
- color = colors.contentColor(isEnabled).value,
- style = Theme.typography.body3,
- modifier = Modifier.fillMaxWidth(),
- )
- }
- }
- }
- }
- }
- }
-}
-
-@Suppress("ComplexMethod")
-@Composable
-fun DefaultTextField(
- value: String,
- onValueChange: (String) -> Unit,
- placeholder: String,
- modifier: Modifier = Modifier,
- onExpandedChange: (Boolean) -> Unit,
- colors: ColorsTextInputYeaHub = TextInputColorsDefaults.defaultColors(),
- isFocused: Boolean = false,
- isEnabled: Boolean = true,
- isError: Boolean = false,
- singleLine: Boolean = true,
- shape: Shape = RoundedCornerShape(12.dp),
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- readOnly: Boolean = false,
- leadingIcon: (@Composable (() -> Unit))? = null,
- trailingIcon: (@Composable (() -> Unit))? = null,
-) {
- val containerColor by colors.containerColor(isEnabled)
- val contentColor by colors.contentColor(isEnabled)
- val focusManager = LocalFocusManager.current
- val keyboardController = LocalSoftwareKeyboardController.current
-
- val state = when {
- !isEnabled -> TextInputState.Disabled
- isError -> TextInputState.Error
- isFocused -> TextInputState.Focused
- else -> TextInputState.Default
- }
-
- val iconColor = when (state) {
- TextInputState.Default -> Theme.colors.black300
- TextInputState.Disabled -> Theme.colors.black300
- else -> Theme.colors.black900
- }
-
- val defaultBorder = when (state) {
- TextInputState.Focused -> TextInputColorsDefaults.activeBorder()
- TextInputState.Error -> TextInputColorsDefaults.errorBorder()
- else -> TextInputColorsDefaults.defaultsBorder()
- }
-
- fun provideIconColor(
- slot: (@Composable () -> Unit)?,
- color: Color,
- ): (@Composable () -> Unit)? =
- slot?.let { content ->
- {
- CompositionLocalProvider(LocalContentColor provides color) {
- content()
- }
- }
- }
-
- val leadingIconSlot = provideIconColor(leadingIcon, iconColor)
-
- val trailingIconSlot = provideIconColor(trailingIcon, iconColor)
-
- OutlinedTextField(
- value = value,
- onValueChange = onValueChange,
- readOnly = readOnly,
- placeholder = {
- Text(
- text = placeholder,
- color = Theme.colors.black300,
- style = Theme.typography.body3,
- )
- },
- modifier = modifier
- .width(328.dp)
- .height(58.dp),
- enabled = isEnabled,
- singleLine = singleLine,
- leadingIcon = leadingIconSlot,
- trailingIcon = trailingIconSlot,
- isError = isError,
- shape = shape,
- textStyle = Theme.typography.body3,
- colors = OutlinedTextFieldDefaults.colors(
- cursorColor = if (state == TextInputState.Focused) contentColor else Color.Transparent,
- unfocusedTextColor = contentColor,
- focusedTextColor = contentColor,
- disabledTextColor = contentColor,
- errorTextColor = contentColor,
- unfocusedContainerColor = containerColor,
- focusedContainerColor = containerColor,
- disabledContainerColor = containerColor,
- errorContainerColor = containerColor,
- unfocusedBorderColor = defaultBorder,
- focusedBorderColor = TextInputColorsDefaults.focusBorder(),
- disabledBorderColor = TextInputColorsDefaults.defaultsBorder(),
- errorBorderColor = TextInputColorsDefaults.errorBorder(),
- ),
- keyboardActions = KeyboardActions(
- onSearch = {
- onExpandedChange(false)
- keyboardController?.hide()
- focusManager.clearFocus(force = true)
- },
- ),
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- interactionSource = interactionSource,
- )
-}
-
-object TextInputColorsDefaults {
- @Composable
- fun defaultColors(
- contentColor: Color = Theme.colors.black900,
- containerColor: Color = Theme.colors.white900,
- disabledContentColor: Color = Theme.colors.black600,
- disabledContainerColor: Color = Theme.colors.black25,
- ): ColorsTextInputYeaHub {
- return ColorsTextInputYeaHub(
- containerColor = containerColor,
- contentColor = contentColor,
- disabledContentColor = disabledContentColor,
- disabledContainerColor = disabledContainerColor,
- )
- }
-
- @Composable
- fun defaultsBorder(): Color = Theme.colors.black50
-
- @Composable
- fun activeBorder(): Color = Theme.colors.purple700
-
- @Composable
- fun errorBorder(): Color = Theme.colors.red700
-
- @Composable
- fun focusBorder(): Color = Theme.colors.purple500
-}
-
-@Immutable
-data class ColorsTextInputYeaHub(
- private val containerColor: Color,
- private val contentColor: Color,
- private val disabledContentColor: Color,
- private val disabledContainerColor: Color,
-) {
- @Composable
- fun containerColor(enabled: Boolean): State {
- return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
- }
-
- @Composable
- fun contentColor(enabled: Boolean): State {
- return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
- }
-}
-
-data class TextInputParams(
- val value: String,
- val onValueChange: (String) -> Unit,
- val onExpandedChange: (Boolean) -> Unit,
- val placeholder: String,
- val modifier: Modifier = Modifier,
- val isEnabled: Boolean = true,
- val isError: Boolean = false,
- val isExpanded: Boolean = false,
- val isFocus: Boolean = false,
- val colors: ColorsTextInputYeaHub = getTextInputColors(),
- val singleLine: Boolean = true,
- val leadingIcon: (@Composable () -> Unit)? = {
- Icon(
- painter = painterResource(R.drawable.icon_search),
- contentDescription = "Поиск",
- modifier = modifier
- .width(20.dp)
- .height(20.dp),
- )
- },
-)
-
-fun getTextInputColors(): ColorsTextInputYeaHub {
- return ColorsTextInputYeaHub(
- contentColor = Color(0xFF191919),
- containerColor = Color(0xFFFFFFFF),
- disabledContentColor = Color(0xFF5E5E5E),
- disabledContainerColor = Color(0xFFF4F4F4),
- )
-}
-
-class TextInputParamsProvider : PreviewParameterProvider {
- override val values = sequenceOf(
- TextInputParams(
- value = "",
- onValueChange = {},
- onExpandedChange = {},
- placeholder = "placeholder",
- isEnabled = true,
- isError = false,
- ),
- TextInputParams(
- value = "text",
- onValueChange = {},
- onExpandedChange = {},
- placeholder = "placeholder",
- isEnabled = true,
- isError = false,
- isFocus = true,
- ),
- TextInputParams(
- value = "",
- onValueChange = {},
- onExpandedChange = {},
- placeholder = "placeholder",
- isEnabled = false,
- isError = false,
- ),
- TextInputParams(
- value = "",
- onValueChange = {},
- onExpandedChange = {},
- placeholder = "placeholder",
- isEnabled = true,
- isError = true,
- ),
- TextInputParams(
- value = "text",
- onValueChange = {},
- onExpandedChange = {},
- placeholder = "placeholder",
- isEnabled = true,
- isError = false,
- isFocus = false,
- ),
- )
-}
-
-@StaticPreview
-@Composable
-fun TextInputPreview(
- @PreviewParameter(TextInputParamsProvider::class) params: TextInputParams,
-) {
- Box(
- Modifier
- .background(Color.White)
- .padding(10.dp),
- ) {
- DefaultTextField(
- value = params.value,
- onValueChange = params.onValueChange,
- onExpandedChange = params.onExpandedChange,
- placeholder = params.placeholder,
- isEnabled = params.isEnabled,
- isError = params.isError,
- colors = params.colors,
- isFocused = params.isFocus,
- leadingIcon = params.leadingIcon,
- )
- }
-}
-
-@StandardScreenSizePreview
-@Composable
-fun TextInputDynamicPreview(
- @PreviewParameter(TextInputPreviewProvider::class) params: Pair>,
-) {
- val mockViewModel = object : SuggestionsViewModel() {
- init {
- onTextChanged(params.first)
- onQueryChange(params.first, params.second)
- }
- }
- ScreenSuggestions(viewModel = mockViewModel)
-}
-
-@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
-@Composable
-fun ScreenSuggestions(
- modifier: Modifier = Modifier,
- viewModel: SuggestionsViewModel,
-) {
- val suggestions = viewModel.suggestions.value
- val text = viewModel.text.value
- var expanded by remember { mutableStateOf(text.isNotEmpty()) }
- Column(
- modifier = modifier
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- ) {
- TextInput(
- value = text,
- onValueChange = {
- viewModel.onTextChanged(it)
- },
- label = "Поиск",
- suggestions = suggestions,
- onQueryChanged = { viewModel.onQueryChange(it, suggestions) },
- expanded = expanded,
- onExpandedChange = { expanded = it },
- )
- }
-}
-
-class TextInputPreviewProvider : PreviewParameterProvider>> {
- override val values: Sequence>> = sequenceOf(
- Pair("", emptyList()),
- Pair("", listOf("кофе", "компьютер", "кот")),
- Pair("теле", listOf("телефон", "телевизор")),
- Pair("телек", listOf("телефон", "телевизор")),
- )
-}
\ No newline at end of file
diff --git a/core/utils/src/main/java/ru/yeahub/core_utils/validation/EmailValidator.kt b/core/utils/src/main/java/ru/yeahub/core_utils/validation/EmailValidator.kt
new file mode 100644
index 00000000..68a2cdb1
--- /dev/null
+++ b/core/utils/src/main/java/ru/yeahub/core_utils/validation/EmailValidator.kt
@@ -0,0 +1,10 @@
+package ru.yeahub.core_utils.validation
+
+import android.util.Patterns
+
+object EmailValidator {
+
+ fun isValid(email: String): Boolean {
+ return email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
+ }
+}
diff --git a/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidationError.kt b/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidationError.kt
new file mode 100644
index 00000000..44055ec1
--- /dev/null
+++ b/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidationError.kt
@@ -0,0 +1,11 @@
+package ru.yeahub.core_utils.validation
+
+/**
+ * Типы ошибок валидации пароля.
+ */
+enum class PasswordValidationError {
+ TOO_SHORT,
+ NO_UPPERCASE,
+ NO_DIGIT,
+ NO_SPECIAL_CHAR
+}
diff --git a/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidator.kt b/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidator.kt
new file mode 100644
index 00000000..d6526555
--- /dev/null
+++ b/core/utils/src/main/java/ru/yeahub/core_utils/validation/PasswordValidator.kt
@@ -0,0 +1,28 @@
+package ru.yeahub.core_utils.validation
+
+/**
+ * Универсальный валидатор паролей для проекта YeaHub.
+ */
+object PasswordValidator {
+
+ private const val MIN_PASSWORD_LENGTH = 8
+
+ /**
+ * Проверяет пароль на соответствие требованиям безопасности.
+ * Возвращает набор PasswordValidationError, если требования не выполнены.
+ */
+ fun validate(password: String): Set = buildSet {
+ if (password.length < MIN_PASSWORD_LENGTH) {
+ add(PasswordValidationError.TOO_SHORT)
+ }
+ if (!password.any { it.isUpperCase() }) {
+ add(PasswordValidationError.NO_UPPERCASE)
+ }
+ if (!password.any { it.isDigit() }) {
+ add(PasswordValidationError.NO_DIGIT)
+ }
+ if (!password.any { !it.isLetterOrDigit() }) {
+ add(PasswordValidationError.NO_SPECIAL_CHAR)
+ }
+ }
+}
From ae52c4b1a52cbb58c5eecd4f026f4a86a3cb0bea Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:51:36 +0300
Subject: [PATCH 17/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-71:=20CreateQuizScreen.=20QOL=20=D0=B8=D0=B7?=
=?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F-2=20(#126)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-71: Добавление нужных текстов для entry-point'а
* ANDR-71: Добавление сущности тренажера в бизнес логике главного экрана
* ANDR-71: Добавление обработки команды по тренажеру
* ANDR-71: Добавлена функция обработки навигации к тренажеру и передается в параметр главного экрана
* ANDR-71: Тест Превью с кнопкой тренажера
* ANDR-71: Использование лямбды навигации для отправки комманд
* ANDR-71: Изменена внешняя функция навигации-входа в фичу тренажера
* ANDR-71: Добавлена реализация InterviewTrainerFeatureImpl. Внутри регистрация графа, обработка навигации назад и обработка навигации к экрану тренировки
* ANDR-71: правки по ktlint'у
* ANDR-71: ScreenMapper теперь класс
* ANDR-71: Константы названий экранов интервью-тренажера
* ANDR-71: Реворк навигации
* ANDR-71: Улучшение экрана. Подключение вьюмодели и команд
* ANDR-71: Вторичные Модули DI
* ANDR-71: Вторичный маппер DI модуль
* ANDR-71: Основной DI модуль экрана
* ANDR-71: Подключение модуля тренажера во все приложение
* ANDR-71: Подключение KOIN модуля
* ANDR-71: Исправлен DI модуль для юзкейса
* ANDR-71: Добавил экрану скролл
* ANDR-71: Навигация с главного экрана на первый экран тренажера (CreateQuizScreen)
* ANDR-71: Создание путей для экрана тренажера
* ANDR-71: Убран хардкод путь экрана CreateQuiz
* ANDR-71: Исправлен класс теста
* ANDR-71: проставлен is
* ANDR-71: добавлен строковой ресурс названия 1 экрана тренажера для прокидывания по навигации
* ANDR-71: используем pathManager для навигации к экрану тренировки
* ANDR-71: handleCommand приватная
* ANDR-71: перегрузка метода получения состояния
* ANDR-71: переименован юзкейс
* ANDR-71: Рефактор DI. Все модули объединены в один
* ANDR-71: коммент к DI
* ANDR-71: Подключение Immutable Collections
* ANDR-71: Удаление наружнего апи интерфейса
* ANDR-71: Оптимизация навигации
* ANDR-71: Навигация теперь принимает не строку, а число айди ресурса
* ANDR-71: исправлены тесты
* ANDR-71: В комментарии вставлены TODO()
* ANDR-71: Обработка ошибок при запросе
* ANDR-71: строковые ресурсы для contentDescription
* ANDR-71: Изменение верстки экрана загрузки; добавление передаваемого параметра отступов
* ANDR-71: небольшой рефактор основного экрана (Box вынесен внутрь экранов, контекст не передается, а создается внутри каждой @Composable, убран хардкод contentDescription)
* ANDR-71: добавлен дополнительный ресурс
* ANDR-71: убрана зависимость от класса TextOrResource (чтоб избавиться от создания контекстов)
* ANDR-71: убран ненужный пустой первичный конструктор
* ANDR-71: Добавлен IO диспатчер на получения списка специализаций
* ANDR-71: State теперь неизменяемый
* ANDR-71: Обработка ошибок вынесена в маппер
* ANDR-71: Новая верстка экрана загрузки
* ANDR-71: Убрано получение стейта через делегат ради оптимизации рекомпозиций
* Revert "ANDR-71: Обработка ошибок вынесена в маппер"
This reverts commit 6fdea11b2d108107feb92b3a97d1fb3a5f3db192.
* ANDR-71: переменные mock переименованы в preview
---
.../presentation/CreateQuizState.kt | 1 +
.../impl/createQuiz/ui/CreateQuizScreen.kt | 47 +++++++++----------
.../createQuiz/ui/CreateQuizScreenLoading.kt | 41 +++++++++++-----
3 files changed, 54 insertions(+), 35 deletions(-)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
index 74b00c64..0ac69675 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
@@ -3,6 +3,7 @@ package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
+@Immutable
sealed interface CreateQuizState {
//Изначальный
data object Loading : CreateQuizState
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index 86bed65f..bd77a3e8 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -23,9 +23,10 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -75,7 +76,7 @@ fun CreateQuizScreen(
) {
val viewModel: CreateQuizViewModel = koinViewModel()
- val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val screenState = viewModel.screenState.collectAsStateWithLifecycle()
HandleCommand(
commandFlow = viewModel.commands,
@@ -91,7 +92,7 @@ fun CreateQuizScreen(
@Composable
private fun ScreenUI(
- state: CreateQuizState,
+ state: State,
onEvent: (CreateQuizEvent) -> Unit,
titleTopAppBar: TextOrResource,
) {
@@ -104,11 +105,11 @@ private fun ScreenUI(
)
}
) { paddingValues ->
- when (state) {
+ when (val currentState = state.value) {
CreateQuizState.Loading -> CreateQuizLoading(paddingValues = paddingValues)
is CreateQuizState.Error -> ErrorScreen(
- error = state.throwable.localizedMessage,
+ error = currentState.throwable.localizedMessage,
errorText = TextOrResource.Resource(R.string.error_screen_text),
titleText = TextOrResource.Resource(R.string.title_error_screen_text),
backText = TextOrResource.Resource(R.string.back_error_screen_text),
@@ -117,9 +118,9 @@ private fun ScreenUI(
)
is CreateQuizState.Loaded -> BaseCreateQuizScreen(
- specializations = state.specializations,
- selectedSpecializationId = state.selectedSpecializationId,
- questionsCount = state.questionsCount,
+ specializations = currentState.specializations,
+ selectedSpecializationId = currentState.selectedSpecializationId,
+ questionsCount = currentState.questionsCount,
onSpecializationClick = { id ->
onEvent(CreateQuizEvent.OnSpecializationClick(specializationId = id))
},
@@ -286,8 +287,8 @@ private fun ChooseQuestionsCountBlock(
private fun QuestionCounter(
onPlusQuestionCountClick: (count: Int) -> Unit,
onMinusQuestionCountClick: (count: Int) -> Unit,
- modifier: Modifier = Modifier,
count: Int,
+ modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
@@ -413,9 +414,7 @@ class CreateQuizScreenStateParamProvider : PreviewParameterProvider
+ val previewDomainList = testSpecializations.map { voSpec ->
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
@@ -445,38 +444,38 @@ internal fun DynamicPreviewUI() {
): DomainSpecializationListResponse {
delay(RESPONSE_DELAY)
return DomainSpecializationListResponse(
- total = mockDomainList.size.toLong(),
- data = mockDomainList
+ total = previewDomainList.size.toLong(),
+ data = previewDomainList
)
}
}
- val mockViewModel = viewModelCreator {
+ val previewViewModel = viewModelCreator {
CreateQuizViewModel(mockUseCase, CreateQuizScreenMapper())
}
- val mockState by mockViewModel.screenState.collectAsState()
+ val previewState = previewViewModel.screenState.collectAsState()
LaunchedEffect(Unit) {
delay(RESPONSE_DELAY)
//Изначальное кол-во == 1
- mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1))
+ previewViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1))
delay(RESPONSE_DELAY)
// должно быть 2
- mockViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2))
+ previewViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2))
delay(RESPONSE_DELAY)
// должно быть 3
- mockViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3))
+ previewViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3))
delay(RESPONSE_DELAY)
// должно быть снова 2
- mockViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(27))
+ previewViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(27))
// С изначально выбранного Frontend Dev должно быть выбрано Android Dev
}
ProvidePreviewCompositionLocals {
ScreenUI(
- state = mockState,
- onEvent = mockViewModel::onEvent,
+ state = previewState,
+ onEvent = previewViewModel::onEvent,
titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
)
}
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
index 483c5a75..37216f35 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreenLoading.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -25,6 +26,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.valentinilk.shimmer.shimmer
import ru.yeahub.core_ui.component.SecondaryButton
@@ -51,7 +53,7 @@ fun CreateQuizLoading(
PlaceHolderBlock()
- Spacer(modifier = Modifier.weight(1f))
+ Spacer(Modifier.weight(1f))
DisabledStartQuizButton()
}
@@ -59,9 +61,7 @@ fun CreateQuizLoading(
}
@Composable
-private fun PlaceHolderBlock(
- modifier: Modifier = Modifier,
-) {
+private fun PlaceHolderBlock(modifier: Modifier = Modifier) {
Card(
modifier = modifier
.fillMaxWidth()
@@ -76,17 +76,36 @@ private fun PlaceHolderBlock(
.background(Color.LightGray, shape = RoundedCornerShape(4.dp))
)
- Spacer(modifier = Modifier.height(12.dp))
+ Spacer(Modifier.height(24.dp))
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(640.dp)
- .background(Color.LightGray, shape = RoundedCornerShape(4.dp))
- )
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(
+ space = 12.dp,
+ alignment = Alignment.Start
+ ),
+ verticalArrangement = Arrangement.spacedBy(
+ space = 12.dp,
+ alignment = Alignment.Top
+ ),
+ ) {
+ repeat(12) {
+ LoadingSpecializationButton(width = 240.dp)
+ }
+ }
+ Spacer(Modifier.height(24.dp))
}
}
+@Composable
+private fun LoadingSpecializationButton(width: Dp) {
+ Box(
+ modifier = Modifier
+ .width(width)
+ .height(40.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(24.dp))
+ )
+}
+
@Composable
private fun DisabledStartQuizButton(
modifier: Modifier = Modifier,
From 9c61fa175316487030b238123692e7b24d2c4a35 Mon Sep 17 00:00:00 2001
From: xMODDIIx <91845532+xMODDIIx@users.noreply.github.com>
Date: Wed, 1 Apr 2026 18:33:18 +0300
Subject: [PATCH 18/28] =?UTF-8?q?ANDR-95-fix-text-field:=20=D0=B4=D0=BE?=
=?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B2=D1=82=D0=BE=D1=80=D0=BE?=
=?UTF-8?q?=D0=B9=20modifier=20=D0=B4=D0=BB=D1=8F=20OutlinedTextField?=
=?UTF-8?q?=E2=80=A6=20(#134)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-95-fix-text-field: добавил второй modifier для OutlinedTextField. Обновил шрифты и отступы согласно дизайну в фигме
* ANDR-95-fix-text-field: добавилен параметр по умолчанию - readOnly: Boolean = false, и проброшен в остальные функции
---
.../ru/yeahub/core_ui/component/TextField.kt | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
index 2f1b8de4..422a38c0 100644
--- a/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/TextField.kt
@@ -132,9 +132,11 @@ internal fun CoreTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
+ textFieldModifier: Modifier = Modifier,
title: String? = null,
placeholder: String? = null,
error: TextOrResource? = null,
+ readOnly: Boolean = false,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
@@ -162,18 +164,18 @@ internal fun CoreTextField(
Column(
modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (title != null) {
Text(
text = title,
- style = Theme.typography.body2Accent,
+ style = Theme.typography.body7,
color = colors.titleColor
)
}
OutlinedTextField(
- modifier = Modifier
+ modifier = textFieldModifier
.fillMaxWidth()
.onFocusChanged { onFocusChanged(it.isFocused) }
.then(
@@ -190,13 +192,15 @@ internal fun CoreTextField(
onValueChange(newTextFieldValue.text)
}
},
+ textStyle = Theme.typography.body7,
+ readOnly = readOnly,
enabled = enabled,
isError = error != null,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
placeholder = placeholder?.let {
- { Text(text = it, style = Theme.typography.body2, color = colors.placeholderColor) }
+ { Text(text = it, style = Theme.typography.body7, color = colors.placeholderColor) }
},
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
@@ -264,9 +268,11 @@ fun PrimaryTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
+ textFieldModifier: Modifier = Modifier,
title: String? = null,
placeholder: String? = null,
error: TextOrResource? = null,
+ readOnly: Boolean = false,
leadingIcon: Painter? = null,
leadingIconContentDescription: String? = null,
trailingIcon: Painter? = null,
@@ -284,8 +290,10 @@ fun PrimaryTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
+ textFieldModifier = textFieldModifier,
title = title,
placeholder = placeholder,
+ readOnly = readOnly,
error = error,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
@@ -346,6 +354,7 @@ fun SearchTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
+ textFieldModifier: Modifier = Modifier,
title: String? = null,
placeholder: String,
suggestions: List,
@@ -366,6 +375,7 @@ fun SearchTextField(
value = value,
title = title,
onValueChange = onValueChange,
+ textFieldModifier = textFieldModifier,
placeholder = placeholder,
enabled = enabled,
colors = colors,
From 31c4dd97d43cd879aead11b6bf63e6e1d7def72f Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Fri, 3 Apr 2026 17:54:18 +0300
Subject: [PATCH 19/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-72:=20=D0=A7=D0=B5=D1=82=D0=B2=D0=B5=D1=80=D1=82?=
=?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF=20InterviewQuizScreen.?=
=?UTF-8?q?=20=D0=9D=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D1=8F=20(#1?=
=?UTF-8?q?33)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-72: использование Parcelize для передачи между экранами
* ANDR-72: дополнение навигации
---
.../interview-trainer/impl/build.gradle.kts | 1 +
.../impl/InterviewTrainerFeatureImpl.kt | 68 +++++++++++++++++++
.../presentation/VoQuestionWithAnswer.kt | 8 ++-
3 files changed, 75 insertions(+), 2 deletions(-)
diff --git a/feature/interview-trainer/impl/build.gradle.kts b/feature/interview-trainer/impl/build.gradle.kts
index b6dbb5eb..494c89ce 100644
--- a/feature/interview-trainer/impl/build.gradle.kts
+++ b/feature/interview-trainer/impl/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.parcelize)
}
android {
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
index 50eb3940..af512c14 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
@@ -8,12 +8,18 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizResult
import ru.yeahub.interview_trainer.impl.createQuiz.ui.CreateQuizScreen
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizResult
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.VoQuestionWithAnswer
+import ru.yeahub.interview_trainer.impl.interviewQuiz.ui.InterviewQuizScreen
import ru.yeahub.navigation_api.FeatureApi
import ru.yeahub.navigation_api.FeatureRoute
import ru.yeahub.navigation_api.NavigationPathManager
import timber.log.Timber
private const val TITLE_TOP_APP_BAR = "title"
+private const val SPECIALIZATION_ID = "specializationId"
+private const val QUESTIONS_COUNT = "questionsCount"
+private const val QUIZ_ANSWERS_KEY = "quizAnswersKey"
private const val NOT_FOUND_NUMBER = 404
class InterviewTrainerFeatureImpl : FeatureApi {
@@ -34,6 +40,12 @@ class InterviewTrainerFeatureImpl : FeatureApi {
val createQuizRoute =
"$featurePath/${FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME}/{$TITLE_TOP_APP_BAR}"
+ // Регистрируем путь экрана тренировки
+ // (interview_trainer/interview_quiz/{titleId}/{specializationId}/{questionsCount})
+ val interviewQuizRoute =
+ "$featurePath/${FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_SCREEN_NAME}" +
+ "/{$TITLE_TOP_APP_BAR}/{$SPECIALIZATION_ID}/{$QUESTIONS_COUNT}"
+
Timber.d("InterviewTrainerFeatureImpl registerGraph: currentPath: $createQuizRoute")
navGraphBuilder.composable(
@@ -67,6 +79,38 @@ class InterviewTrainerFeatureImpl : FeatureApi {
}
CreateQuizScreen(onResult = onResult, titleTopAppBarResId = titleTopAppBarResId)
}
+
+ navGraphBuilder.composable(
+ route = interviewQuizRoute,
+ arguments = listOf(
+ navArgument(TITLE_TOP_APP_BAR) { type = NavType.IntType },
+ navArgument(SPECIALIZATION_ID) { type = NavType.StringType },
+ navArgument(QUESTIONS_COUNT) { type = NavType.StringType },
+ )
+ ) { backStackEntry ->
+ val titleTopAppBarResId =
+ backStackEntry.arguments?.getInt(TITLE_TOP_APP_BAR)
+ ?: NOT_FOUND_NUMBER
+
+ InterviewQuizScreen(
+ onResult = { result ->
+ when (result) {
+ is InterviewQuizResult.NavigateBack -> handleBackNavigation(
+ pathManager = pathManager,
+ navController = navController
+ )
+
+ is InterviewQuizResult.NavigateToInterviewQuizResultScreen -> handleQuizResultNavigation(
+ pathManager = pathManager,
+ navController = navController,
+ featurePath = featurePath,
+ titleTopAppBarResId = titleTopAppBarResId,
+ questionsWithAnswersList = result.questionsWithAnswersList
+ )
+ }
+ }
+ )
+ }
}
/**
@@ -113,4 +157,28 @@ class InterviewTrainerFeatureImpl : FeatureApi {
navController.navigate(interviewQuizRoute)
pathManager.setCurrentPath(interviewQuizRoute)
}
+
+ /**
+ * Обработка навигации к экрану результата тренировки (InterviewQuizResultScreen)
+ */
+ private fun handleQuizResultNavigation(
+ pathManager: NavigationPathManager,
+ navController: NavHostController,
+ featurePath: String,
+ titleTopAppBarResId: Int,
+ questionsWithAnswersList: List
+ ) {
+ navController.currentBackStackEntry
+ ?.savedStateHandle
+ ?.set(QUIZ_ANSWERS_KEY, ArrayList(questionsWithAnswersList))
+
+ val interviewQuizResultRoute =
+ "$featurePath/${FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_RESULT_SCREEN_NAME}" +
+ "/$titleTopAppBarResId"
+
+ Timber.d("InterviewTrainerFeatureImpl registerGraph: $interviewQuizResultRoute")
+
+ navController.navigate(interviewQuizResultRoute)
+ pathManager.setCurrentPath(interviewQuizResultRoute)
+ }
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
index cb3e3002..9d9b9690 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/VoQuestionWithAnswer.kt
@@ -1,16 +1,20 @@
package ru.yeahub.interview_trainer.impl.interviewQuiz.presentation
+import android.os.Parcelable
import androidx.compose.runtime.Immutable
+import kotlinx.parcelize.Parcelize
@Immutable
+@Parcelize
data class VoQuestionWithAnswer(
val id: Long,
val title: String,
val shortAnswer: String,
val userAnswer: QuizAnswerResult
-)
+) : Parcelable
-enum class QuizAnswerResult {
+@Parcelize
+enum class QuizAnswerResult : Parcelable {
KNOWN,
UNKNOWN
}
From 77b6fde6aa50ccf28042bc59c069dd1672106309 Mon Sep 17 00:00:00 2001
From: Artem_Mih <149761873+PanMobile@users.noreply.github.com>
Date: Tue, 14 Apr 2026 11:29:03 +0300
Subject: [PATCH 20/28] =?UTF-8?q?[Dynamic=20Preview]=20ANDR-94:=20=D0=9D?=
=?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B5=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8?=
=?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2?=
=?UTF-8?q?=D1=8C=D1=8E=20=20(#137)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-94: Аннотация для динамического превью
* ANDR-94: Подключение KOIN к core-ui
* ANDR-94: Создание провайдер-функции для динамического превью и документации к ней
* ANDR-94: Улучшение функции + убраны лишние комменты
* ANDR-94: Мелкие правки динамик превью
* ANDR-94: Создание примера
* ANDR-94: Убрал старый пример динам. превью и также убрал ненужную надслойку-функцию
* ANDR-94: Новый пример динам. превью
* ANDR-94: Слегка обновил описание провайдер функции. Больше конкретики, что превью смотрит только один стейт
---
core/ui/build.gradle.kts | 3 +
.../example/dynamicPreview/CountViewModel.kt | 19 +++
.../example/dynamicPreview/DynamicPreview.kt | 71 +++++++++++
.../example/dynamicPreview/MyViewModel.kt | 15 ---
.../example/dynamicPreview/PreviewExample.kt | 111 ++++++++++--------
.../impl/ui/PublicCollectionsScreen.kt | 4 +-
.../impl/ui/SpecializationScreen.kt | 4 +-
7 files changed, 158 insertions(+), 69 deletions(-)
create mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/CountViewModel.kt
create mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/DynamicPreview.kt
delete mode 100644 core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/MyViewModel.kt
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 87d774ec..a9aaa555 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -51,6 +51,9 @@ dependencies {
implementation(libs.androidx.runtime.android)
implementation(libs.androidx.icons)
+ implementation(libs.koin.core)
+ implementation(libs.koin.compose)
+
implementation(libs.coil.compose)
implementation(libs.jsoup)
implementation(libs.compose.markdown)
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/CountViewModel.kt b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/CountViewModel.kt
new file mode 100644
index 00000000..3e41d2c4
--- /dev/null
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/CountViewModel.kt
@@ -0,0 +1,19 @@
+package ru.yeahub.core_ui.example.dynamicPreview
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.lifecycle.ViewModel
+
+@Immutable
+open class CountViewModel() : ViewModel() {
+
+ val counterState = mutableIntStateOf(0)
+
+ fun increment() {
+ counterState.intValue++
+ }
+
+ fun decrement() {
+ counterState.intValue--
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/DynamicPreview.kt b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/DynamicPreview.kt
new file mode 100644
index 00000000..da86c559
--- /dev/null
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/DynamicPreview.kt
@@ -0,0 +1,71 @@
+package ru.yeahub.core_ui.example.dynamicPreview
+
+import android.content.res.Configuration
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.tooling.preview.Preview
+import org.koin.compose.KoinIsolatedContext
+import org.koin.dsl.ModuleDeclaration
+import org.koin.dsl.koinApplication
+import org.koin.dsl.module
+
+@Preview(
+ name = "Dynamic normal screen (phone) preview",
+ group = "DynamicPreviews",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+ device = "id:pixel_9",
+)
+// Пока не будет темной темы - неактивно
+//@Preview(
+// name = "Dark theme preview",
+// group = "DynamicPreviews",
+// uiMode = Configuration.UI_MODE_NIGHT_YES,
+//)
+@Preview(
+ name = "Dynamic wide screen (tablet) preview",
+ group = "DynamicPreviews",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+ device = "id:Nexus 9",
+)
+annotation class DynamicPreview
+
+/**
+ * Провайдер для создания динамических превью с реальными зависимостями
+ *
+ * Динамическое превью позволяет запустить экран с настоящей ViewModel'ью и DI-зависимостями
+ * для проверки изменения параметров в рамках одного Loaded стейта (Loading и Error посмотреть можно отдельно)
+ *
+ * Каждый вариант превью получает изолированный Koin-контекст через [KoinIsolatedContext],
+ * поэтому несколько превью одного экрана (телефон + планшет) не конфликтуют между собой.
+ *
+ * Для полноценной проверки активируйте Interactive Mode у динамического превью:
+ * нажмите на иконку трёх точек () в тулбаре над превью и выберете Start Interactive Mode
+ *
+ * **Ограничение:** ViewModel не должна выполнять реальные IO-операции, т.к. превью работает
+ * без сети и без БД. Мокайте UseCase'ы с заранее подготовленными данными.
+ * Подробнее об ограничениях превью:
+ * [Compose Previews](https://developer.android.com/develop/ui/compose/tooling/previews)
+ *
+ * @param moduleDeclaration лямбда, задающая Koin-модуль с зависимостями экрана (UseCase, ViewModel)
+ * @param content Composable-контент для отображения в превью (не обязательно весь экран)
+ */
+
+@Composable
+fun ProvideDynamicPreview(
+ moduleDeclaration: ModuleDeclaration,
+ content: @Composable () -> Unit,
+) {
+ val koinApp = remember {
+ koinApplication {
+ modules(module(moduleDeclaration = moduleDeclaration))
+ }
+ }
+
+ KoinIsolatedContext(
+ context = koinApp,
+ content = { CompositionLocalProvider(content = content) }
+ )
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/MyViewModel.kt b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/MyViewModel.kt
deleted file mode 100644
index c924d966..00000000
--- a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/MyViewModel.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package ru.yeahub.core_ui.example.dynamicPreview
-
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.lifecycle.ViewModel
-
-open class MyViewModel() : ViewModel() {
-
- private var _textState = mutableIntStateOf(0)
- val textState: State = _textState
-
- fun increment(number: Int = 0) {
- _textState.intValue = _textState.intValue + number + 1
- }
-}
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/PreviewExample.kt b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/PreviewExample.kt
index 0fed3c74..3734bd2c 100644
--- a/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/PreviewExample.kt
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/example/dynamicPreview/PreviewExample.kt
@@ -1,84 +1,95 @@
package ru.yeahub.core_ui.example.dynamicPreview
-import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import org.koin.androidx.compose.koinViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import ru.yeahub.core_ui.theme.colors
-@Preview
+@DynamicPreview
@Composable
-fun DynamicPreview(@PreviewParameter(NumbersPreviewProvider::class) numbers: Int) {
- val mockViewModel = object : MyViewModel() {
- init {
- increment(numbers)
+internal fun DynamicPreviewV2() {
+ ProvideDynamicPreview(
+ moduleDeclaration = {
+ viewModel { CountViewModel() }
+ },
+ content = {
+ ScreenCount(
+ modifier = Modifier.fillMaxSize(),
+ viewModel = koinViewModel()
+ )
}
- }
- ProvidePreviewCompositionLocals {
- ScreenCount(viewModel = mockViewModel)
- }
+ )
}
-@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
-fun ScreenCount(
+private fun ScreenCount(
modifier: Modifier = Modifier,
textModifier: Modifier = Modifier,
buttonModifier: Modifier = Modifier,
textOnButtonModifier: Modifier = Modifier,
- viewModel: MyViewModel,
+ viewModel: CountViewModel,
) {
- val textState = viewModel.textState.value
+ val textState = viewModel.counterState.intValue
- Column(
- modifier = modifier
- .width(100.dp)
- .height(100.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- modifier = textModifier.padding(bottom = 30.dp),
- text = "Current Count: $textState",
- fontSize = 30.sp
- )
- Spacer(modifier = Modifier.height(30.dp))
- Button(
- modifier = buttonModifier
- .width(200.dp)
- .height(60.dp),
- onClick = { viewModel.increment() },
+ Scaffold { paddingValues ->
+ Column(
+ modifier = modifier.padding(paddingValues),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
) {
Text(
- modifier = textOnButtonModifier,
- style = TextStyle(fontSize = 18.sp),
- text = "Count++"
+ modifier = textModifier.padding(bottom = 30.dp),
+ text = "Current Count: $textState",
+ fontSize = 30.sp
)
- }
- }
-}
-@Composable
-fun ProvidePreviewCompositionLocals(content: @Composable () -> Unit) {
- CompositionLocalProvider(
- content = content,
- )
-}
+ Spacer(Modifier.height(32.dp))
+
+ Button(
+ modifier = buttonModifier
+ .width(200.dp)
+ .height(60.dp),
+ onClick = viewModel::increment,
+ colors = ButtonDefaults.buttonColors(containerColor = colors.green600)
+ ) {
+ Text(
+ modifier = textOnButtonModifier,
+ style = TextStyle(fontSize = 18.sp),
+ text = "Count++"
+ )
+ }
-class NumbersPreviewProvider : PreviewParameterProvider {
- override val values: Sequence = sequenceOf(22, 45, 6666, 123563)
+ Spacer(Modifier.height(16.dp))
+
+ Button(
+ modifier = buttonModifier
+ .width(200.dp)
+ .height(60.dp),
+ onClick = viewModel::decrement,
+ colors = ButtonDefaults.buttonColors(containerColor = colors.red600)
+ ) {
+ Text(
+ modifier = textOnButtonModifier,
+ style = TextStyle(fontSize = 18.sp),
+ text = "Count--"
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/feature/public-collections/impl/src/main/java/ru/yeahub/public_collections/impl/ui/PublicCollectionsScreen.kt b/feature/public-collections/impl/src/main/java/ru/yeahub/public_collections/impl/ui/PublicCollectionsScreen.kt
index 4005552f..6034cd9b 100644
--- a/feature/public-collections/impl/src/main/java/ru/yeahub/public_collections/impl/ui/PublicCollectionsScreen.kt
+++ b/feature/public-collections/impl/src/main/java/ru/yeahub/public_collections/impl/ui/PublicCollectionsScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -61,7 +62,6 @@ import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
import ru.yeahub.core_ui.component.CollectionCard
import ru.yeahub.core_ui.component.ErrorScreen
-import ru.yeahub.core_ui.example.dynamicPreview.ProvidePreviewCompositionLocals
import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.Theme
import ru.yeahub.core_ui.theme.Theme.colors
@@ -524,7 +524,7 @@ fun PublicCollectionsScreenDynamicPreview() {
mockViewModel.onEvent(PublicCollectionsScreenEvent.LoadNextPage)
}
- ProvidePreviewCompositionLocals {
+ CompositionLocalProvider {
ScreenUI(
listState = lazyListState,
state = state,
diff --git a/feature/selection-specializations/impl/src/main/java/ru/yeahub/selection_specializations/impl/ui/SpecializationScreen.kt b/feature/selection-specializations/impl/src/main/java/ru/yeahub/selection_specializations/impl/ui/SpecializationScreen.kt
index 39a70177..1e0a343b 100644
--- a/feature/selection-specializations/impl/src/main/java/ru/yeahub/selection_specializations/impl/ui/SpecializationScreen.kt
+++ b/feature/selection-specializations/impl/src/main/java/ru/yeahub/selection_specializations/impl/ui/SpecializationScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -42,7 +43,6 @@ import ru.yeahub.core_ui.component.ErrorScreen
import ru.yeahub.core_ui.component.PrimaryButton
import ru.yeahub.core_ui.component.SpecializationButton
import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
-import ru.yeahub.core_ui.example.dynamicPreview.ProvidePreviewCompositionLocals
import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
@@ -368,7 +368,7 @@ fun SpecializationDynamicPreview() {
mockSpecializationViewModel.onEvent(SpecializationScreenEvent.LoadNextPage)
}
- ProvidePreviewCompositionLocals {
+ CompositionLocalProvider {
ScreenUI(
headerText = headerText,
screenState = mockState,
From c05fb17369290774008b4a716ee9e513a07917dd Mon Sep 17 00:00:00 2001
From: Yrun <155985757+Yrun00@users.noreply.github.com>
Date: Fri, 17 Apr 2026 20:16:41 +0400
Subject: [PATCH 21/28] ANDR-101: SkillButton-Refactor (#140)
* skillButton refactor v1
* skillButton refactor v2 with preview
* fix names
* fix sizes
* return contentPaddingDefault
* delete contentPaddingDefault
* small change
* add interactionSource
---
.../yeahub/core_ui/component/SkillButton.kt | 924 ++++++++----------
1 file changed, 384 insertions(+), 540 deletions(-)
diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
index 3b604821..f7a4cadb 100644
--- a/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
+++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/SkillButton.kt
@@ -1,637 +1,481 @@
package ru.yeahub.core_ui.component
+import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
-import ru.yeahub.core_ui.component.ButtonColorDefaults.activeBorder
-import ru.yeahub.core_ui.component.ButtonColorDefaults.defaultsBorder
+import coil.compose.AsyncImage
import ru.yeahub.core_ui.theme.Theme
+import ru.yeahub.core_ui.theme.YeaHubTheme
import ru.yeahub.ui.R
-private val contentPaddingDefault = PaddingValues(
- start = 12.dp,
- end = 12.dp,
- top = 6.dp,
- bottom = 6.dp
-)
-private val contentPaddingLendingDefault = PaddingValues(
- start = 6.dp,
- end = 6.dp,
- top = 6.dp,
- bottom = 6.dp
-)
-private val contentPadding_Default = PaddingValues(
- start = 0.dp,
- end = 1.dp,
- top = 0.dp,
- bottom = 0.dp
+@Immutable
+data class CoreSkillButtonColors(
+ val activeContainerColor: Color,
+ val inactiveContainerColor: Color,
+ val activeContentColor: Color,
+ val inactiveContentColor: Color,
+ val selectedBorderColor: Color,
+ val unselectedBorderColor: Color,
)
-@Composable
-fun SkillButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = false,
- activeButton: Boolean = false,
- imageLeft: Int? = null,
- imageRight: Int? = null,
- elevation: Dp = 0.dp,
- text: String? = null,
- buttonWithoutBackground: Boolean = false,
- imageSizeLeftWith: Dp = 0.dp,
- imageSizeLeftHigh: Dp = 0.dp,
- fillButton: Boolean = false,
- colors: ColorsButtonYeaHub = ButtonColorDefaults.firstButtonColors(),
- shape: Shape = RoundedCornerShape(12.dp),
- onRightIconClick: (() -> Unit)? = null,
- contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
-) {
- DefaultButton(
- onClick = onClick,
- modifier = modifier,
- enabled = enabled,
- activeButton = activeButton,
- colors = colors,
- elevation = elevation,
- shape = shape,
- contentPadding = contentPadding,
- onRightIconClick = onRightIconClick,
- imageRight = imageRight,
- imageLeft = imageLeft,
- imageSizeLeftHigh = imageSizeLeftHigh,
- imageSizeLeftWith = imageSizeLeftWith,
- text = text,
- fillButton = fillButton,
- buttonWithoutBackground = buttonWithoutBackground
+object CoreSkillButtonDefaults {
+ val Shape: Shape = RoundedCornerShape(12.dp)
+
+ @Composable
+ fun colors(
+ activeContainerColor: Color = Theme.colors.white900,
+ inactiveContainerColor: Color = Theme.colors.black50,
+ activeContentColor: Color = Theme.colors.black800,
+ inactiveContentColor: Color = Theme.colors.black600,
+ selectedBorderColor: Color = Theme.colors.purple700,
+ unselectedBorderColor: Color = Theme.colors.black200,
+ ): CoreSkillButtonColors = CoreSkillButtonColors(
+ activeContainerColor = activeContainerColor,
+ inactiveContainerColor = inactiveContainerColor,
+ activeContentColor = activeContentColor,
+ inactiveContentColor = inactiveContentColor,
+ selectedBorderColor = selectedBorderColor,
+ unselectedBorderColor = unselectedBorderColor,
)
}
-@Suppress("ComplexMethod")
+/**
+ * Базовый stateless компонент кнопки-скилла.
+ *
+ * Используйте специализированные обёртки вместо этого компонента напрямую:
+ * [SkillButton], [SkillButtonWithBorder], [SkillButtonWithDeleteButton].
+ *
+ * @param onClick колбэк нажатия на кнопку.
+ * @param modifier модификатор.
+ * @param selected выбрана ли кнопка. Определяет цвет бордера: фиолетовый при true, серый при false.
+ * @param active визуальный режим. true — цветная, false — серый фон ([CoreSkillButtonColors.inactiveContainerColor]),
+ * серый текст ([CoreSkillButtonColors.inactiveContentColor]), иконка обесцвечена.
+ * @param enabled кликабельность тела кнопки. Не влияет на визуальное отображение.
+ * @param showBorder отображать ли бордер. false — бордер отсутствует независимо от [selected].
+ * @param text текст кнопки. Если null — текст не отображается.
+ * @param leadingIconRes drawable-ресурс иконки слева. Приоритет над [leadingIconUrl].
+ * @param leadingIconUrl URL иконки слева (Coil). Используется если [leadingIconRes] равен null.
+ * @param trailingIcon drawable-ресурс иконки справа. Если null — иконка не отображается.
+ * @param onTrailingIconClick колбэк нажатия на правую иконку.
+ * @param leftIconSize размер левой иконки.
+ * @param colors цвета. Используйте [CoreSkillButtonDefaults.colors] для переопределения.
+ * @param shape форма кнопки. По умолчанию [CoreSkillButtonDefaults.Shape].
+ * @param elevation тень кнопки.
+ * @param contentPadding внутренние отступы.
+ */
@Composable
-fun DefaultButton(
+fun CoreSkillButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ active: Boolean = true,
enabled: Boolean = true,
- elevation: Dp = 0.dp,
- fillButton: Boolean = false,
- imageSizeLeftWith: Dp = 30.dp,
- imageSizeLeftHigh: Dp = 30.dp,
- activeButton: Boolean = false,
- colors: ColorsButtonYeaHub = ButtonColorDefaults.firstButtonColors(),
- buttonWithoutBackground: Boolean = false,
- shape: Shape = ButtonDefaults.shape,
- contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+ showBorder: Boolean = true,
text: String? = null,
- imageLeft: Int? = null,
- imageRight: Int? = null,
- onRightIconClick: (() -> Unit)? = null,
+ @DrawableRes leadingIconRes: Int? = null,
+ leadingIconUrl: String? = null,
+ @DrawableRes trailingIcon: Int? = null,
+ onTrailingIconClick: (() -> Unit)? = null,
+ leftIconSize: DpSize = DpSize(20.dp, 20.dp),
+ colors: CoreSkillButtonColors = CoreSkillButtonDefaults.colors(),
+ shape: Shape = CoreSkillButtonDefaults.Shape,
+ elevation: Dp = 0.dp,
+ contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 6.dp),
) {
- val defaultColor: Color = Theme.colors.white900
- val purple = Theme.colors.purple700
- val black = Theme.colors.black800
-
- var showBorder by remember { mutableStateOf(activeButton) }
- val interactionSource = remember { MutableInteractionSource() }
-
- val containerColor by colors.containerColor(enabled)
- val contentColor by colors.contentColor(enabled)
-
- var newContainerColor by remember { mutableStateOf(containerColor) }
- var newContentColor by remember { mutableStateOf(contentColor) }
+ val border = when {
+ !showBorder -> null
+ selected -> BorderStroke(1.dp, colors.selectedBorderColor)
+ else -> BorderStroke(1.dp, colors.unselectedBorderColor)
+ }
- val onSurfaceClick: () -> Unit = {
- if (fillButton) {
- showBorder = !showBorder
+ val containerColor = if (active) colors.activeContainerColor else colors.inactiveContainerColor
+ val contentColor = if (active) colors.activeContentColor else colors.inactiveContentColor
- newContentColor = if (showBorder) {
- defaultColor
- } else {
- black
- }
- }
- if (enabled) {
- showBorder = !showBorder
- }
- if (buttonWithoutBackground) {
- newContentColor = if (newContentColor == black) {
- purple
- } else {
- black
- }
- }
- onClick()
- }
- val border = when {
- showBorder && !fillButton && !buttonWithoutBackground -> activeBorder()
- fillButton && enabled && activeButton -> activeBorder()
- fillButton && enabled -> null
- buttonWithoutBackground -> null
- else -> defaultsBorder()
+ val iconColorFilter = if (!active) {
+ ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
+ } else {
+ null
}
- val updatedContentColor =
- if (newContentColor == black && buttonWithoutBackground && activeButton) {
- purple
- } else if (newContentColor == black && fillButton && activeButton) {
- black
- } else if (newContentColor == Theme.colors.white900) {
- black
- } else {
- newContentColor
- }
+
+ val interactionSource = remember { MutableInteractionSource() }
Surface(
- onClick = { onSurfaceClick() },
+ onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
- color = if (activeButton && fillButton) {
- defaultColor
- } else {
- if (buttonWithoutBackground) Color.Transparent else newContainerColor
- },
- contentColor = updatedContentColor,
+ color = containerColor,
+ contentColor = contentColor,
border = border,
+ shadowElevation = elevation,
interactionSource = interactionSource,
- shadowElevation = elevation
) {
- CompositionLocalProvider(LocalContentColor provides updatedContentColor) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
Row(
modifier = Modifier.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
- content = {
- imageLeft?.let {
- val imageLeftPainter = when {
- imageLeft == R.drawable.ellipse -> {
- painterResource(id = R.drawable.ellipse)
- }
- enabled -> {
- painterResource(id = R.drawable.icon_true_button)
- }
- else -> {
- painterResource(id = R.drawable.icon_false_button)
- }
- }
- Image(
- modifier = Modifier
- .height(imageSizeLeftHigh)
- .width(imageSizeLeftWith),
- painter = imageLeftPainter,
- contentDescription = "Left Icon",
- )
- }
- text?.let {
+ ) {
+ if (leadingIconRes != null) {
+ Image(
+ painter = painterResource(id = leadingIconRes),
+ contentDescription = null,
+ modifier = Modifier
+ .height(leftIconSize.height)
+ .width(leftIconSize.width),
+ colorFilter = iconColorFilter,
+ )
+ } else if (leadingIconUrl != null) {
+ AsyncImage(
+ model = leadingIconUrl,
+ contentDescription = null,
+ modifier = Modifier
+ .height(leftIconSize.height)
+ .width(leftIconSize.width),
+ colorFilter = iconColorFilter,
+ )
+ }
+
+ text?.let {
+ if (leadingIconRes != null || leadingIconUrl != null) {
Spacer(modifier = Modifier.width(6.dp))
- Text(
- text = text,
- style = Theme.typography.body3Accent,
- )
- }
- imageRight?.let {
- Spacer(modifier = Modifier.width(8.dp))
- val imageRPainter: Painter = painterResource(id = imageRight)
- Image(
- modifier = Modifier
- .height(20.dp)
- .width(20.dp)
- .clickable {
- onRightIconClick?.invoke()
- },
- painter = imageRPainter,
- contentDescription = "Close",
- )
}
+ Text(
+ text = it,
+ style = Theme.typography.body3Accent,
+ )
}
- )
+
+ trailingIcon?.let {
+ Spacer(modifier = Modifier.width(8.dp))
+ Image(
+ painter = painterResource(id = it),
+ contentDescription = null,
+ modifier = Modifier
+ .height(20.dp)
+ .width(20.dp)
+ .clickable(
+ indication = null,
+ interactionSource = interactionSource,
+ ) { onTrailingIconClick?.invoke() },
+ )
+ }
+ }
}
}
}
-object ButtonColorDefaults {
- @Composable
- fun firstButtonColors(
- contentColor: Color = Theme.colors.black800,
- containerColor: Color = Theme.colors.white900,
- disabledContentColor: Color = Theme.colors.black200,
- disabledContainerColor: Color = Theme.colors.white900,
- ): ColorsButtonYeaHub {
- return ColorsButtonYeaHub(
- contentColor = contentColor,
- containerColor = containerColor,
- disabledContentColor = disabledContentColor,
- disabledContainerColor = disabledContainerColor
- )
- }
-
- @Composable
- fun defaultsBorder(
- width: Dp = 1.dp,
- borderColor: Color = Theme.colors.black200,
- ): BorderStroke {
- return BorderStroke(
- width = width,
- color = borderColor
- )
- }
-
- @Composable
- fun activeBorder(
- width: Dp = 1.dp,
- borderColor: Color = Theme.colors.purple700,
- ): BorderStroke {
- return BorderStroke(
- width = width,
- color = borderColor
- )
- }
+/**
+ * Кнопка выбора специализации или категории.
+ *
+ * Поддерживает два визуальных режима, переключаемых через [active]:
+ * - активный ([active] = true): цветная иконка, фиолетовый бордер при выборе, не выбранная - без бордера.
+ * - неактивный ([active] = false): серый фон, обесцвеченная иконка, без бордера.
+ *
+ * @param text текст кнопки.
+ * @param selected выбрана ли кнопка. В активном режиме определяет фиолетовый бордер.
+ * @param onClick колбэк нажатия на кнопку.
+ * @param modifier модификатор.
+ * @param active визуальный режим. true — цветная, false — серая/обесцвеченная.
+ * @param enabled кликабельность тела кнопки. Не влияет на визуальное отображение.
+ * @param leadingIconRes drawable-ресурс иконки слева. Приоритет над [leadingIconUrl].
+ * @param leadingIconUrl URL иконки слева (Coil). Используется если [leadingIconRes] равен null.
+ * @param iconSize размер левой иконки.
+ * @param contentPadding внутренние отступы.
+ * @param colors цвета. Используйте [CoreSkillButtonDefaults.colors] для переопределения.
+ */
+@Composable
+fun SkillButton(
+ text: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ active: Boolean = true,
+ enabled: Boolean = true,
+ @DrawableRes leadingIconRes: Int? = null,
+ leadingIconUrl: String? = null,
+ iconSize: DpSize = DpSize(20.dp, 20.dp),
+ contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
+ colors: CoreSkillButtonColors = CoreSkillButtonDefaults.colors(),
+) {
+ CoreSkillButton(
+ onClick = onClick,
+ modifier = modifier,
+ selected = selected,
+ active = active,
+ enabled = enabled,
+ showBorder = selected && active,
+ text = text,
+ leadingIconRes = leadingIconRes,
+ leadingIconUrl = leadingIconUrl,
+ leftIconSize = iconSize,
+ colors = colors,
+ contentPadding = contentPadding,
+ )
}
-// Класс для управления цветами кнопок
-@Immutable
-data class ColorsButtonYeaHub(
- private val contentColor: Color,
- private val containerColor: Color,
- private val disabledContentColor: Color,
- private val disabledContainerColor: Color,
-) : ButtonColors1 {
-
- @Composable
- override fun containerColor(enabled: Boolean): State {
- return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
- }
-
- @Composable
- override fun contentColor(enabled: Boolean): State {
- return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
- }
+/**
+ * Кнопка выбора скилла. Бордер присутствует всегда: серый в невыбранном состоянии,
+ * фиолетовый при выборе. Иконка опциональна.
+ *
+ * @param text текст кнопки.
+ * @param selected выбрана ли кнопка. Определяет цвет бордера.
+ * @param onClick колбэк нажатия на кнопку.
+ * @param modifier модификатор.
+ * @param enabled кликабельность тела кнопки. Не влияет на визуальное отображение.
+ * @param leadingIconRes drawable-ресурс иконки слева. Приоритет над [leadingIconUrl].
+ * @param leadingIconUrl URL иконки слева (Coil). Используется если [leadingIconRes] равен null.
+ * @param leftIconSize размер левой иконки.
+ * @param contentPadding внутренние отступы.
+ * @param colors цвета. Используйте [CoreSkillButtonDefaults.colors] для переопределения.
+ */
+@Composable
+fun SkillButtonWithBorder(
+ text: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = false,
+ @DrawableRes leadingIconRes: Int? = null,
+ leadingIconUrl: String? = null,
+ leftIconSize: DpSize = DpSize(20.dp, 20.dp),
+ contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
+ colors: CoreSkillButtonColors = CoreSkillButtonDefaults.colors(),
+) {
+ CoreSkillButton(
+ onClick = onClick,
+ modifier = modifier,
+ selected = selected,
+ active = true,
+ enabled = enabled,
+ showBorder = true,
+ text = text,
+ leadingIconRes = leadingIconRes,
+ leadingIconUrl = leadingIconUrl,
+ leftIconSize = leftIconSize,
+ colors = colors,
+ contentPadding = contentPadding,
+ )
}
-@Stable
-interface ButtonColors1 {
- @Composable
- fun containerColor(enabled: Boolean): State
-
- @Composable
- fun contentColor(enabled: Boolean): State
+/**
+ * Кнопка отображения выбранного скилла с возможностью удаления.
+ *
+ * Всегда имеет фиолетовый бордер. Тело кнопки по умолчанию не кликабельно
+ * и не реагирует на нажатия. Иконка удаления кликабельна независимо от [enabled].
+ *
+ * @param text текст кнопки.
+ * @param onDeleteClick колбэк нажатия на иконку удаления.
+ * @param modifier модификатор.
+ * @param enabled кликабельность тела кнопки. Не влияет на визуальное отображение.
+ * По умолчанию false — тело некликабельно, нет ripple-эффекта.
+ * @param leadingIconRes drawable-ресурс иконки слева. Приоритет над [leadingIconUrl].
+ * @param leadingIconUrl URL иконки слева (Coil). Используется если [leadingIconRes] равен null.
+ * @param leftIconSize размер левой иконки.
+ * @param contentPadding внутренние отступы.
+ * @param colors цвета. Используйте [CoreSkillButtonDefaults.colors] для переопределения.
+ */
+@Composable
+fun SkillButtonWithDeleteButton(
+ text: String,
+ onDeleteClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = false,
+ @DrawableRes leadingIconRes: Int? = null,
+ leadingIconUrl: String? = null,
+ leftIconSize: DpSize = DpSize(20.dp, 20.dp),
+ @DrawableRes trailingIcon: Int = R.drawable.icon_button_close,
+ contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
+ colors: CoreSkillButtonColors = CoreSkillButtonDefaults.colors(),
+) {
+ CoreSkillButton(
+ onClick = {},
+ modifier = modifier,
+ selected = true,
+ active = true,
+ enabled = enabled,
+ showBorder = true,
+ text = text,
+ leadingIconRes = leadingIconRes,
+ leadingIconUrl = leadingIconUrl,
+ trailingIcon = trailingIcon,
+ onTrailingIconClick = onDeleteClick,
+ leftIconSize = leftIconSize,
+ colors = colors,
+ contentPadding = contentPadding,
+ )
}
-data class SkillButtonParams(
- val onClick: () -> Unit,
- val enabled: Boolean = true,
- val activeButton: Boolean = false,
- val imageLeft: Int? = null,
- val imageRight: Int? = null,
- val elevation: Dp = 0.dp,
- val colors: ColorsButtonYeaHub = getButtonColors(),
- val text: String? = null,
- val buttonWithoutBackground: Boolean = false,
- val imageSizeLeftWith: Dp = 0.dp,
- val imageSizeLeftHigh: Dp = 0.dp,
- val fillButton: Boolean = false,
- val shape: Shape = RoundedCornerShape(12.dp),
- val contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+internal data class SkillButtonPreviewState(
+ val name: String,
+ val selected: Boolean,
+ val active: Boolean,
+ val hasIcon: Boolean,
)
-class SkillButtonParamsProvider : PreviewParameterProvider {
-
+internal class SkillButtonPreviewProvider : PreviewParameterProvider {
override val values = sequenceOf(
- // firstButton
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_false_button,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- // withoutIconsButton
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingDefault,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
+ SkillButtonPreviewState(
+ "1. Не выбрана, активная",
+ selected = false,
+ active = true,
+ hasIcon = false,
),
- // lendingButton
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_true_button,
- fillButton = true,
- elevation = 8.dp,
- imageSizeLeftWith = 36.dp,
- imageSizeLeftHigh = 36.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
+ SkillButtonPreviewState(
+ "2. Выбрана, активная",
+ selected = true,
+ active = true,
+ hasIcon = false,
),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_true_button,
- fillButton = true,
- elevation = 8.dp,
- imageSizeLeftWith = 36.dp,
- imageSizeLeftHigh = 36.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
+ SkillButtonPreviewState(
+ "3. С иконкой, не выбрана",
+ selected = false,
+ active = true,
+ hasIcon = true,
),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_false_button,
- fillButton = true,
- elevation = 8.dp,
- imageSizeLeftWith = 36.dp,
- imageSizeLeftHigh = 36.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
+ SkillButtonPreviewState(
+ "4. С иконкой, выбрана",
+ selected = true,
+ active = true,
+ hasIcon = true,
),
- //canDelete
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageRight = R.drawable.icon_button_close,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
+ SkillButtonPreviewState("5. Неактивная", selected = false, active = false, hasIcon = false),
+ SkillButtonPreviewState(
+ "6. Неактивная, с иконкой",
+ selected = false,
+ active = false,
+ hasIcon = true,
),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageRight = R.drawable.icon_button_close,
- elevation = 8.dp,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_false_button,
- imageRight = R.drawable.icon_button_close,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- // buttonWithoutBackground
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPadding_Default,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 20.dp,
- imageSizeLeftHigh = 20.dp,
- buttonWithoutBackground = true,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPadding_Default,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 20.dp,
- imageSizeLeftHigh = 20.dp,
- buttonWithoutBackground = true,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPadding_Default,
- imageLeft = R.drawable.icon_false_button,
- imageSizeLeftWith = 20.dp,
- imageSizeLeftHigh = 20.dp,
- buttonWithoutBackground = true,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- // kompaniButton
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.ellipse,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.ellipse,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.ellipse,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- ),
- // iconsButton
- SkillButtonParams(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 24.dp,
- imageSizeLeftHigh = 24.dp,
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = true,
- activeButton = true,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageSizeLeftWith = 24.dp,
- imageSizeLeftHigh = 24.dp,
- onClick = { },
- colors = getButtonColors(),
- ),
- SkillButtonParams(
- enabled = false,
- activeButton = false,
- contentPadding = contentPaddingLendingDefault,
- imageLeft = R.drawable.icon_false_button,
- imageSizeLeftWith = 24.dp,
- imageSizeLeftHigh = 24.dp,
- onClick = { },
- colors = getButtonColors(),
- )
)
}
-fun getButtonColors(): ColorsButtonYeaHub {
- return ColorsButtonYeaHub(
- contentColor = Color(color = 0xFF303030),
- containerColor = Color(color = 0xFFFFFFFF),
- disabledContentColor = Color(color = 0xFFBABABA),
- disabledContainerColor = Color(color = 0xFFFFFFFF)
+@Preview(showBackground = true)
+@Composable
+internal fun SkillButtonPreview(
+ @PreviewParameter(SkillButtonPreviewProvider::class) state: SkillButtonPreviewState,
+) {
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.black25)
+ .padding(16.dp),
+ ) {
+ SkillButton(
+ text = "Figma",
+ selected = state.selected,
+ active = state.active,
+ leadingIconRes = if (state.hasIcon) R.drawable.icon_true_button else null,
+ iconSize = DpSize(20.dp, 20.dp),
+ onClick = {},
+ )
+ }
+ }
+}
+
+internal data class SkillButtonWithBorderPreviewState(
+ val name: String,
+ val selected: Boolean,
+ val hasIcon: Boolean,
+)
+
+internal class SkillButtonWithBorderPreviewProvider :
+ PreviewParameterProvider {
+ override val values = sequenceOf(
+ SkillButtonWithBorderPreviewState("1. Не выбрана", selected = false, hasIcon = false),
+ SkillButtonWithBorderPreviewState("2. Выбрана", selected = true, hasIcon = false),
+ SkillButtonWithBorderPreviewState(
+ "3. С иконкой, не выбрана",
+ selected = false,
+ hasIcon = true,
+ ),
+ SkillButtonWithBorderPreviewState("4. С иконкой, выбрана", selected = true, hasIcon = true),
)
}
@Preview(showBackground = true)
@Composable
-fun SkillButtonPreview(
- @PreviewParameter(SkillButtonParamsProvider::class) params: SkillButtonParams,
+internal fun SkillButtonWithBorderPreview(
+ @PreviewParameter(SkillButtonWithBorderPreviewProvider::class) state: SkillButtonWithBorderPreviewState,
) {
- Box(modifier = Modifier.padding(10.dp)) {
- SkillButton(
- onClick = params.onClick,
- enabled = params.enabled,
- activeButton = params.activeButton,
- imageLeft = params.imageLeft,
- imageRight = params.imageRight,
- elevation = params.elevation,
- text = params.text,
- buttonWithoutBackground = params.buttonWithoutBackground,
- imageSizeLeftWith = params.imageSizeLeftWith,
- imageSizeLeftHigh = params.imageSizeLeftHigh,
- fillButton = params.fillButton,
- colors = params.colors,
- contentPadding = params.contentPadding
- )
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.white900)
+ .padding(16.dp),
+ ) {
+ SkillButtonWithBorder(
+ text = "Figma",
+ selected = state.selected,
+ leadingIconRes = if (state.hasIcon) R.drawable.icon_true_button else null,
+ leftIconSize = DpSize(20.dp, 20.dp),
+ onClick = {},
+ )
+ }
}
}
-@Preview
+internal data class SkillButtonWithDeleteButtonPreviewState(
+ val name: String,
+ val hasIcon: Boolean,
+)
+
+internal class SkillButtonWithDeleteButtonPreviewProvider :
+ PreviewParameterProvider {
+ override val values = sequenceOf(
+ SkillButtonWithDeleteButtonPreviewState("1. Без иконки", hasIcon = false),
+ SkillButtonWithDeleteButtonPreviewState("2. С иконкой", hasIcon = true),
+ )
+}
+
+@Preview(showBackground = true)
@Composable
-fun ExampleScreen() {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- SkillButton(
- enabled = true,
- activeButton = false,
- contentPadding = contentPaddingDefault,
- imageLeft = R.drawable.icon_true_button,
- imageRight = R.drawable.icon_button_close,
- imageSizeLeftWith = 30.dp,
- imageSizeLeftHigh = 30.dp,
- text = "Figma",
- onClick = { },
- colors = getButtonColors(),
- )
+internal fun SkillButtonWithDeleteButtonPreview(
+ @PreviewParameter(SkillButtonWithDeleteButtonPreviewProvider::class) state: SkillButtonWithDeleteButtonPreviewState,
+) {
+ YeaHubTheme {
+ Box(
+ modifier = Modifier
+ .background(Theme.colors.white900)
+ .padding(16.dp),
+ ) {
+ SkillButtonWithDeleteButton(
+ text = "Figma",
+ leadingIconRes = if (state.hasIcon) R.drawable.icon_true_button else null,
+ leftIconSize = DpSize(20.dp, 20.dp),
+ onDeleteClick = {},
+ )
+ }
}
}
From 4a831a3f941366feddda218c21a39c39b3f615be Mon Sep 17 00:00:00 2001
From: arte123ras
Date: Sat, 25 Apr 2026 15:25:52 +0300
Subject: [PATCH 22/28] =?UTF-8?q?=D0=92=D1=82=D0=BE=D1=80=D0=BE=D0=B9=20?=
=?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF,=20ANDR-64:=20data+domain=20(#144)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-64: Добавил в ViewModel метод mapToState, чтобы ловить данные с экрана Ивана через savedStateHandle.
* ANDR-64: Перемещение файлов
* ANDR-64: Перемеместил логику mapToState в ScreenMapper, добавил зависимость
* ANDR-64: Правки ktlint
* ANDR-64: изменил factory на single
* ANDR-64: Добавил тесты
---
app/src/main/java/ru/yeahub/Application.kt | 4 +-
.../InterviewQuizResultScreenMapper.kt | 43 ------
.../InterviewQuizResultViewModel.kt | 29 ----
.../di/InterviewQuizResultModule.kt | 20 +++
.../InterviewQuizResultEvent.kt | 2 +-
.../InterviewQuizResultScreenMapper.kt | 35 +++++
.../InterviewQuizResultState.kt | 0
.../InterviewQuizResultViewModel.kt | 35 +++++
.../ui/InterviewQuizResultScreen.kt | 7 +-
.../InterviewQuizResultScreenMapperTest.kt | 129 ++++++++++++++++++
10 files changed, 227 insertions(+), 77 deletions(-)
delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
delete mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/di/InterviewQuizResultModule.kt
rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/{ => presentation}/InterviewQuizResultEvent.kt (64%)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultScreenMapper.kt
rename feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/{ => presentation}/InterviewQuizResultState.kt (100%)
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultViewModel.kt
create mode 100644 feature/interview-trainer/impl/src/test/java/test/InterviewQuizResultScreenMapperTest.kt
diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt
index f36bfecf..8d2a0b8d 100644
--- a/app/src/main/java/ru/yeahub/Application.kt
+++ b/app/src/main/java/ru/yeahub/Application.kt
@@ -8,6 +8,7 @@ import ru.yeahub.example_details.impl.detailsFeatureModule
import ru.yeahub.example_home.impl.data.di.questionsMainFeatureModule
import ru.yeahub.example_profile.impl.profileFeatureModule
import ru.yeahub.interview_trainer.impl.createQuiz.di.createQuizModule
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.di.interviewQuizResultModule
import ru.yeahub.navigation_impl.navigationPathModule
import ru.yeahub.network_impl.networkModule
import ru.yeahub.public_collections.impl.di.CollectionsFeatureModule
@@ -55,7 +56,8 @@ class Application : Application() {
detailQuestionFeatureModule,
collectionsAndQuestionsFeatureModule,
specializationFeatureModule,
- createQuizModule
+ createQuizModule,
+ interviewQuizResultModule
)
}
// проверка, что модули загружены
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
deleted file mode 100644
index 26ccd35a..00000000
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultScreenMapper.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package ru.yeahub.interview_trainer.impl.interviewQuizResult
-
-import InterviewQuizResultState
-import kotlinx.collections.immutable.persistentListOf
-import ru.yeahub.core_utils.common.TextOrResource
-import ru.yeahub.interview_trainer.impl.R
-
-private const val DEFAULT_PERCENTAGE = 0.75f
-private const val DEFAULT_TOTAL_QUESTIONS = 20
-private const val DEFAULT_NEW_QUESTIONS = 50
-private const val DEFAULT_IN_PROGRESS = 120
-private const val DEFAULT_STUDIED = 12
-private const val DEFAULT_SKILL_SCORE = 60
-private const val DEFAULT_SKILL_MAX = 120
-
-class InterviewQuizResultScreenMapper {
-
- fun getScreenState(): InterviewQuizResultState =
- InterviewQuizResultState.Loaded(
- titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
- overallPercentage = DEFAULT_PERCENTAGE,
- totalQuestions = DEFAULT_TOTAL_QUESTIONS,
- newQuestions = DEFAULT_NEW_QUESTIONS,
- inProgress = DEFAULT_IN_PROGRESS,
- studied = DEFAULT_STUDIED,
- skills = persistentListOf(
- InterviewQuizResultState.Loaded.VoSkillStat("HTML", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
- InterviewQuizResultState.Loaded.VoSkillStat("CSS", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
- InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
- InterviewQuizResultState.Loaded.VoSkillStat("React", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
- InterviewQuizResultState.Loaded.VoSkillStat("PHP", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX),
- InterviewQuizResultState.Loaded.VoSkillStat("JavaScript", DEFAULT_SKILL_SCORE, DEFAULT_SKILL_MAX)
- ),
- questions = persistentListOf(
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false),
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false),
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", true),
- InterviewQuizResultState.Loaded.VoQuestionResult("Что такое Virtual DOM, и как он работает?", false)
- )
- )
-}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
deleted file mode 100644
index cf5ea388..00000000
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultViewModel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.stateIn
-import ru.yeahub.core_utils.BaseViewModel
-import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultEvent
-import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultScreenMapper
-
-private const val STOP_TIMEOUT_MILLIS = 5000L
-
-class InterviewQuizResultViewModel(
- private val mapper: InterviewQuizResultScreenMapper
-) : BaseViewModel() {
-
- val state: StateFlow =
- flow {
- emit(mapper.getScreenState())
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
- initialValue = InterviewQuizResultState.Loading
- )
-
- fun onEvent(event: InterviewQuizResultEvent) {
- // TODO:
- }
-}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/di/InterviewQuizResultModule.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/di/InterviewQuizResultModule.kt
new file mode 100644
index 00000000..00faa88e
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/di/InterviewQuizResultModule.kt
@@ -0,0 +1,20 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation.InterviewQuizResultScreenMapper
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation.InterviewQuizResultViewModel
+
+val interviewQuizResultModule = module {
+
+ single {
+ InterviewQuizResultScreenMapper()
+ }
+
+ viewModel { params ->
+ InterviewQuizResultViewModel(
+ savedStateHandle = params.get(),
+ mapper = get()
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultEvent.kt
similarity index 64%
rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt
rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultEvent.kt
index 7f3e37e5..a1499a7f 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultEvent.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultEvent.kt
@@ -1,4 +1,4 @@
-package ru.yeahub.interview_trainer.impl.interviewQuizResult
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation
sealed interface InterviewQuizResultEvent {
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultScreenMapper.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultScreenMapper.kt
new file mode 100644
index 00000000..2012ec1f
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultScreenMapper.kt
@@ -0,0 +1,35 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation
+
+import InterviewQuizResultState
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.QuizAnswerResult
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.VoQuestionWithAnswer
+
+class InterviewQuizResultScreenMapper {
+
+ fun getScreenState(
+ questions: List
+ ): InterviewQuizResultState.Loaded {
+ val total = questions.size
+ val correct = questions.count { it.userAnswer == QuizAnswerResult.KNOWN }
+
+ return InterviewQuizResultState.Loaded(
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ overallPercentage = if (total == 0) 0f else correct.toFloat() / total,
+ totalQuestions = total,
+ newQuestions = 0,
+ inProgress = 0,
+ studied = correct,
+ skills = persistentListOf(),
+ questions = questions.map { question: VoQuestionWithAnswer ->
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ questionText = question.title,
+ isCorrect = question.userAnswer == QuizAnswerResult.KNOWN
+ )
+ }.toPersistentList(),
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultState.kt
similarity index 100%
rename from feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/InterviewQuizResultState.kt
rename to feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultState.kt
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultViewModel.kt
new file mode 100644
index 00000000..d2569bdf
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/presentation/InterviewQuizResultViewModel.kt
@@ -0,0 +1,35 @@
+package ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation
+
+import InterviewQuizResultState
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
+import ru.yeahub.core_utils.BaseViewModel
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.VoQuestionWithAnswer
+
+private const val STOP_TIMEOUT_MILLIS = 5000L
+
+class InterviewQuizResultViewModel(
+ savedStateHandle: SavedStateHandle,
+ private val mapper: InterviewQuizResultScreenMapper
+) : BaseViewModel() {
+
+ private val questions: List =
+ savedStateHandle["quizAnswersKey"] ?: emptyList()
+
+ val state: StateFlow =
+ flow {
+ emit(mapper.getScreenState(questions))
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
+ initialValue = InterviewQuizResultState.Loading
+ )
+
+ fun onEvent(event: InterviewQuizResultEvent) {
+ // TODO
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
index 5a2f4be7..4aa6903a 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuizResult/ui/InterviewQuizResultScreen.kt
@@ -1,7 +1,6 @@
package ru.yeahub.interview_trainer.impl.interviewQuizResult.ui
import InterviewQuizResultState
-import InterviewQuizResultViewModel
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -56,7 +55,8 @@ import ru.yeahub.core_ui.theme.Theme.typography
import ru.yeahub.core_ui.theme.colors
import ru.yeahub.core_utils.common.TextOrResource
import ru.yeahub.interview_trainer.impl.R
-import ru.yeahub.interview_trainer.impl.interviewQuizResult.InterviewQuizResultEvent
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation.InterviewQuizResultEvent
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation.InterviewQuizResultViewModel
private val H_PADDING = 16.dp
private val V_BLOCK = 16.dp
@@ -106,6 +106,7 @@ private fun ScreenUI(
onBack = {}
)
}
+
is InterviewQuizResultState.Loaded -> {
BaseInterviewQuizResultScreen(
state = currentState,
@@ -302,7 +303,7 @@ private fun OverallProgressStatistics(
totalQuestions: Int,
newQuestions: Int,
inProgress: Int,
- studied: Int
+ studied: Int,
) {
Column(
modifier = Modifier.fillMaxWidth(),
diff --git a/feature/interview-trainer/impl/src/test/java/test/InterviewQuizResultScreenMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizResultScreenMapperTest.kt
new file mode 100644
index 00000000..912e82a4
--- /dev/null
+++ b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizResultScreenMapperTest.kt
@@ -0,0 +1,129 @@
+package test
+
+import kotlinx.collections.immutable.persistentListOf
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.QuizAnswerResult
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.VoQuestionWithAnswer
+import ru.yeahub.interview_trainer.impl.R
+import ru.yeahub.interview_trainer.impl.interviewQuizResult.presentation.InterviewQuizResultScreenMapper
+import ru.yeahub.test.TestArgumentsProvider
+
+class InterviewQuizResultScreenMapperTest {
+
+ @ParameterizedTest
+ @ArgumentsSource(ArgumentsProvider::class)
+ fun getScreenStateTest(
+ testCase: InterviewQuizResultScreenMapperTestCase
+ ) {
+ val result = InterviewQuizResultScreenMapper().getScreenState(
+ questions = testCase.questions
+ )
+
+ assertEquals(testCase.expectedResult, result)
+ }
+
+ data class InterviewQuizResultScreenMapperTestCase(
+ val questions: List,
+ val expectedResult: InterviewQuizResultState.Loaded
+ )
+
+ class ArgumentsProvider :
+ TestArgumentsProvider() {
+
+ override fun testCases(): List = listOf(
+ InterviewQuizResultScreenMapperTestCase(
+ questions = listOf(
+ VoQuestionWithAnswer(
+ id = 1L,
+ title = "Q1",
+ shortAnswer = "A1",
+ userAnswer = QuizAnswerResult.KNOWN,
+
+ ),
+ VoQuestionWithAnswer(
+ id = 2L,
+ title = "Q2",
+ shortAnswer = "A2",
+ userAnswer = QuizAnswerResult.UNKNOWN
+ )
+ ),
+ expectedResult = InterviewQuizResultState.Loaded(
+ overallPercentage = 0.5f,
+ totalQuestions = 2,
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ newQuestions = 0,
+ inProgress = 0,
+ studied = 1,
+ skills = persistentListOf(),
+ questions = persistentListOf(
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ questionText = "Q1",
+ isCorrect = true
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ questionText = "Q2",
+ isCorrect = false
+ )
+ ),
+
+ )
+ ),
+
+
+ InterviewQuizResultScreenMapperTestCase(
+ questions = listOf(
+ VoQuestionWithAnswer(
+ id = 1L,
+ title = "Q1",
+ shortAnswer = "A1",
+ userAnswer = QuizAnswerResult.KNOWN
+ ),
+ VoQuestionWithAnswer(
+ id = 2L,
+ title = "Q2",
+ shortAnswer = "A2",
+ userAnswer = QuizAnswerResult.KNOWN
+ )
+ ),
+ expectedResult = InterviewQuizResultState.Loaded(
+ overallPercentage = 1f,
+ totalQuestions = 2,
+ newQuestions = 0,
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text),
+ inProgress = 0,
+ studied = 2,
+ skills = persistentListOf(),
+ questions = persistentListOf(
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ questionText = "Q1",
+ isCorrect = true
+ ),
+ InterviewQuizResultState.Loaded.VoQuestionResult(
+ questionText = "Q2",
+ isCorrect = true
+ )
+ ),
+
+ )
+ ),
+
+
+ InterviewQuizResultScreenMapperTestCase(
+ questions = emptyList(),
+ expectedResult = InterviewQuizResultState.Loaded(
+ overallPercentage = 0f,
+ totalQuestions = 0,
+ newQuestions = 0,
+ inProgress = 0,
+ studied = 0,
+ skills = persistentListOf(),
+ questions = persistentListOf(),
+ titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
From 921e66cedcfe89414f05dcfb313214b725bd955f Mon Sep 17 00:00:00 2001
From: Artemmih <149761873+PanMobile@users.noreply.github.com>
Date: Mon, 11 May 2026 12:41:51 +0300
Subject: [PATCH 23/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-71:=20CreateQuizScreen.=20QOL=20=D0=B8=D0=B7?=
=?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F-3=20(#147)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-71: Добавление нужных текстов для entry-point'а
* ANDR-71: Добавление сущности тренажера в бизнес логике главного экрана
* ANDR-71: Добавление обработки команды по тренажеру
* ANDR-71: Добавлена функция обработки навигации к тренажеру и передается в параметр главного экрана
* ANDR-71: Тест Превью с кнопкой тренажера
* ANDR-71: Использование лямбды навигации для отправки комманд
* ANDR-71: Изменена внешняя функция навигации-входа в фичу тренажера
* ANDR-71: Добавлена реализация InterviewTrainerFeatureImpl. Внутри регистрация графа, обработка навигации назад и обработка навигации к экрану тренировки
* ANDR-71: правки по ktlint'у
* ANDR-71: ScreenMapper теперь класс
* ANDR-71: Константы названий экранов интервью-тренажера
* ANDR-71: Реворк навигации
* ANDR-71: Улучшение экрана. Подключение вьюмодели и команд
* ANDR-71: Вторичные Модули DI
* ANDR-71: Вторичный маппер DI модуль
* ANDR-71: Основной DI модуль экрана
* ANDR-71: Подключение модуля тренажера во все приложение
* ANDR-71: Подключение KOIN модуля
* ANDR-71: Исправлен DI модуль для юзкейса
* ANDR-71: Добавил экрану скролл
* ANDR-71: Навигация с главного экрана на первый экран тренажера (CreateQuizScreen)
* ANDR-71: Создание путей для экрана тренажера
* ANDR-71: Убран хардкод путь экрана CreateQuiz
* ANDR-71: Исправлен класс теста
* ANDR-71: проставлен is
* ANDR-71: добавлен строковой ресурс названия 1 экрана тренажера для прокидывания по навигации
* ANDR-71: используем pathManager для навигации к экрану тренировки
* ANDR-71: handleCommand приватная
* ANDR-71: перегрузка метода получения состояния
* ANDR-71: переименован юзкейс
* ANDR-71: Рефактор DI. Все модули объединены в один
* ANDR-71: коммент к DI
* ANDR-71: Подключение Immutable Collections
* ANDR-71: Удаление наружнего апи интерфейса
* ANDR-71: Оптимизация навигации
* ANDR-71: Навигация теперь принимает не строку, а число айди ресурса
* ANDR-71: исправлены тесты
* ANDR-71: В комментарии вставлены TODO()
* ANDR-71: Обработка ошибок при запросе
* ANDR-71: строковые ресурсы для contentDescription
* ANDR-71: Изменение верстки экрана загрузки; добавление передаваемого параметра отступов
* ANDR-71: небольшой рефактор основного экрана (Box вынесен внутрь экранов, контекст не передается, а создается внутри каждой @Composable, убран хардкод contentDescription)
* ANDR-71: добавлен дополнительный ресурс
* ANDR-71: убрана зависимость от класса TextOrResource (чтоб избавиться от создания контекстов)
* ANDR-71: убран ненужный пустой первичный конструктор
* ANDR-71: Добавлен IO диспатчер на получения списка специализаций
* ANDR-71: State теперь неизменяемый
* ANDR-71: Обработка ошибок вынесена в маппер
* ANDR-71: Новая верстка экрана загрузки
* ANDR-71: Убрано получение стейта через делегат ради оптимизации рекомпозиций
* Revert "ANDR-71: Обработка ошибок вынесена в маппер"
This reverts commit 6fdea11b2d108107feb92b3a97d1fb3a5f3db192.
* ANDR-71: переменные mock переименованы в preview
* ANDR-71: убран titleId из навигации. Заголовок экрана теперь часть стейта
* ANDR-71: Добавлены динамик превью, новая skillButton. Пофикшена плавающая кнопка изменения кол-ва вопросов
---
.../impl/QuestionMainFeatureImpl.kt | 5 +-
.../impl/InterviewTrainerFeatureImpl.kt | 36 ++----
.../presentation/CreateQuizState.kt | 15 ++-
.../impl/createQuiz/ui/CreateQuizScreen.kt | 109 ++++++------------
4 files changed, 57 insertions(+), 108 deletions(-)
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
index e9d85fb8..c4b2ada9 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/QuestionMainFeatureImpl.kt
@@ -4,7 +4,6 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
-import ru.yeahub.core_utils.common.TextOrResource
import ru.yeahub.example_home.impl.presentation.view.QuestionsMainScreen
import ru.yeahub.navigation_api.FeatureApi
import ru.yeahub.navigation_api.FeatureRoute
@@ -112,14 +111,12 @@ class QuestionMainFeatureImpl : FeatureApi {
pathManager: NavigationPathManager,
navController: NavHostController,
) {
- val titleTopAppBarResId = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
-
// Сбрасываем текущий путь на корневую фичу
pathManager.setCurrentPath(FeatureRoute.InterviewTrainerFeature.FEATURE_NAME)
val createQuizPath = pathManager.createChildPath(
featureName = FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME
- ) + "/" + titleTopAppBarResId.resource
+ )
Timber.d("HomeFeatureImpl handleInterviewTrainerNavigation: Navigating to: $createQuizPath")
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
index af512c14..28eb28cb 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
@@ -16,11 +16,9 @@ import ru.yeahub.navigation_api.FeatureRoute
import ru.yeahub.navigation_api.NavigationPathManager
import timber.log.Timber
-private const val TITLE_TOP_APP_BAR = "title"
private const val SPECIALIZATION_ID = "specializationId"
private const val QUESTIONS_COUNT = "questionsCount"
private const val QUIZ_ANSWERS_KEY = "quizAnswersKey"
-private const val NOT_FOUND_NUMBER = 404
class InterviewTrainerFeatureImpl : FeatureApi {
override fun getFeatureName(): String = FeatureRoute.InterviewTrainerFeature.FEATURE_NAME
@@ -38,27 +36,19 @@ class InterviewTrainerFeatureImpl : FeatureApi {
//Регистрируем путь экрана создания тренировки (interview_trainer/create_quiz/{titleId})
val createQuizRoute =
- "$featurePath/${FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME}/{$TITLE_TOP_APP_BAR}"
+ "$featurePath/${FeatureRoute.InterviewTrainerFeature.CREATE_QUIZ_SCREEN_NAME}"
// Регистрируем путь экрана тренировки
- // (interview_trainer/interview_quiz/{titleId}/{specializationId}/{questionsCount})
+ // (interview_trainer/interview_quiz/{specializationId}/{questionsCount})
val interviewQuizRoute =
"$featurePath/${FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_SCREEN_NAME}" +
- "/{$TITLE_TOP_APP_BAR}/{$SPECIALIZATION_ID}/{$QUESTIONS_COUNT}"
+ "/{$SPECIALIZATION_ID}/{$QUESTIONS_COUNT}"
Timber.d("InterviewTrainerFeatureImpl registerGraph: currentPath: $createQuizRoute")
navGraphBuilder.composable(
route = createQuizRoute,
- arguments = listOf(
- navArgument(TITLE_TOP_APP_BAR) {
- type = NavType.IntType
- }
- )
- ) { backStackEntry ->
- val titleTopAppBarResId =
- backStackEntry.arguments?.getInt(TITLE_TOP_APP_BAR) ?: NOT_FOUND_NUMBER
-
+ ) { _ ->
// Вынос в отдельную переменную для оптимизации (чтоб не пересоздавать лямбду)
val onResult = { result: CreateQuizResult ->
when (result) {
@@ -71,27 +61,21 @@ class InterviewTrainerFeatureImpl : FeatureApi {
pathManager = pathManager,
navController = navController,
featurePath = featurePath,
- titleTopAppBarResId = titleTopAppBarResId,
specializationId = result.specializationId.toString(),
questionsCount = result.questionCount.toString()
)
}
}
- CreateQuizScreen(onResult = onResult, titleTopAppBarResId = titleTopAppBarResId)
+ CreateQuizScreen(onResult = onResult)
}
navGraphBuilder.composable(
route = interviewQuizRoute,
arguments = listOf(
- navArgument(TITLE_TOP_APP_BAR) { type = NavType.IntType },
navArgument(SPECIALIZATION_ID) { type = NavType.StringType },
navArgument(QUESTIONS_COUNT) { type = NavType.StringType },
)
- ) { backStackEntry ->
- val titleTopAppBarResId =
- backStackEntry.arguments?.getInt(TITLE_TOP_APP_BAR)
- ?: NOT_FOUND_NUMBER
-
+ ) { _ ->
InterviewQuizScreen(
onResult = { result ->
when (result) {
@@ -104,7 +88,6 @@ class InterviewTrainerFeatureImpl : FeatureApi {
pathManager = pathManager,
navController = navController,
featurePath = featurePath,
- titleTopAppBarResId = titleTopAppBarResId,
questionsWithAnswersList = result.questionsWithAnswersList
)
}
@@ -143,14 +126,13 @@ class InterviewTrainerFeatureImpl : FeatureApi {
pathManager: NavigationPathManager,
navController: NavHostController,
featurePath: String,
- titleTopAppBarResId: Int,
specializationId: String,
questionsCount: String,
) {
//Регистрируем путь экрана тренировки (interview_trainer/create_quiz/{titleId})
val interviewQuizRoute = featurePath + "/" +
FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_SCREEN_NAME + "/" +
- "$titleTopAppBarResId/$specializationId/$questionsCount"
+ "$specializationId/$questionsCount"
Timber.d("InterviewTrainerFeatureImpl registerGraph: $interviewQuizRoute")
@@ -165,7 +147,6 @@ class InterviewTrainerFeatureImpl : FeatureApi {
pathManager: NavigationPathManager,
navController: NavHostController,
featurePath: String,
- titleTopAppBarResId: Int,
questionsWithAnswersList: List
) {
navController.currentBackStackEntry
@@ -173,8 +154,7 @@ class InterviewTrainerFeatureImpl : FeatureApi {
?.set(QUIZ_ANSWERS_KEY, ArrayList(questionsWithAnswersList))
val interviewQuizResultRoute =
- "$featurePath/${FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_RESULT_SCREEN_NAME}" +
- "/$titleTopAppBarResId"
+ "$featurePath/${FeatureRoute.InterviewTrainerFeature.INTERVIEW_QUIZ_RESULT_SCREEN_NAME}"
Timber.d("InterviewTrainerFeatureImpl registerGraph: $interviewQuizResultRoute")
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
index 0ac69675..1188d9bf 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/presentation/CreateQuizState.kt
@@ -2,17 +2,26 @@ package ru.yeahub.interview_trainer.impl.createQuiz.presentation
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.interview_trainer.impl.R
@Immutable
sealed interface CreateQuizState {
+
+ val titleTopAppBar: TextOrResource
+
//Изначальный
- data object Loading : CreateQuizState
+ data object Loading : CreateQuizState {
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
data class Loaded(
val specializations: ImmutableList,
val selectedSpecializationId: Long,
val questionsCount: Int,
) : CreateQuizState {
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+
@Immutable
data class VoSpecialization(
val id: Long,
@@ -20,5 +29,7 @@ sealed interface CreateQuizState {
)
}
- data class Error(val throwable: Throwable) : CreateQuizState
+ data class Error(val throwable: Throwable) : CreateQuizState {
+ override val titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ }
}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
index bd77a3e8..154d46ea 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/ui/CreateQuizScreen.kt
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -22,34 +23,28 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import org.koin.androidx.compose.koinViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
import ru.yeahub.core_ui.component.ErrorScreen
import ru.yeahub.core_ui.component.PrimaryButton
import ru.yeahub.core_ui.component.SkillButton
import ru.yeahub.core_ui.component.TopAppBarWithBottomBorder
-import ru.yeahub.core_ui.example.dynamicPreview.ProvidePreviewCompositionLocals
+import ru.yeahub.core_ui.example.dynamicPreview.DynamicPreview
+import ru.yeahub.core_ui.example.dynamicPreview.ProvideDynamicPreview
import ru.yeahub.core_ui.example.staticPreview.StaticPreview
import ru.yeahub.core_ui.theme.LocalAppTypography
import ru.yeahub.core_ui.theme.colors
@@ -70,10 +65,7 @@ import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewMo
private val FIGMA_VERTICAL_FIRST_AND_LAST_ELEMENT_PADDING = 24.dp
@Composable
-fun CreateQuizScreen(
- onResult: (CreateQuizResult) -> Unit,
- titleTopAppBarResId: Int,
-) {
+fun CreateQuizScreen(onResult: (CreateQuizResult) -> Unit) {
val viewModel: CreateQuizViewModel = koinViewModel()
val screenState = viewModel.screenState.collectAsStateWithLifecycle()
@@ -86,7 +78,7 @@ fun CreateQuizScreen(
ScreenUI(
state = screenState,
onEvent = viewModel::onEvent,
- titleTopAppBar = TextOrResource.Resource(titleTopAppBarResId)
+ titleTopAppBar = screenState.value.titleTopAppBar
)
}
@@ -247,11 +239,8 @@ private fun ChooseSpecializationBlock(
) {
specializations.forEach { specialization ->
SkillButton(
- contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
- enabled = true,
- activeButton = specialization.id == selectedSpecializationId,
- fillButton = true,
text = specialization.title,
+ selected = specialization.id == selectedSpecializationId,
onClick = { onSpecializationClick(specialization.id) },
)
}
@@ -316,7 +305,8 @@ private fun QuestionCounter(
text = count.toString(),
style = LocalAppTypography.current.body5Accent,
textAlign = TextAlign.Center,
- color = colors.black600
+ color = colors.black600,
+ modifier = Modifier.widthIn(min = 32.dp)
)
IconButton(
@@ -420,7 +410,7 @@ class CreateQuizScreenStateParamProvider : PreviewParameterProvider
DomainSpecialization(id = voSpec.id, title = voSpec.title)
}
@@ -441,58 +431,29 @@ internal fun DynamicPreviewUI() {
val mockUseCase = object : GetSpecializationsListUseCase {
override suspend fun invoke(
request: SpecializationsRequest,
- ): DomainSpecializationListResponse {
- delay(RESPONSE_DELAY)
- return DomainSpecializationListResponse(
- total = previewDomainList.size.toLong(),
- data = previewDomainList
- )
- }
- }
-
- val previewViewModel = viewModelCreator {
- CreateQuizViewModel(mockUseCase, CreateQuizScreenMapper())
- }
-
- val previewState = previewViewModel.screenState.collectAsState()
-
- LaunchedEffect(Unit) {
- delay(RESPONSE_DELAY)
- //Изначальное кол-во == 1
- previewViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(1))
- delay(RESPONSE_DELAY)
- // должно быть 2
- previewViewModel.onEvent(CreateQuizEvent.OnPlusQuestionClick(2))
- delay(RESPONSE_DELAY)
- // должно быть 3
- previewViewModel.onEvent(CreateQuizEvent.OnMinusQuestionClick(3))
- delay(RESPONSE_DELAY)
- // должно быть снова 2
- previewViewModel.onEvent(CreateQuizEvent.OnSpecializationClick(27))
- // С изначально выбранного Frontend Dev должно быть выбрано Android Dev
- }
-
- ProvidePreviewCompositionLocals {
- ScreenUI(
- state = previewState,
- onEvent = previewViewModel::onEvent,
- titleTopAppBar = TextOrResource.Resource(R.string.create_quiz_top_bar_header_text)
+ ): DomainSpecializationListResponse = DomainSpecializationListResponse(
+ total = previewDomainList.size.toLong(),
+ data = previewDomainList
)
}
-}
-typealias ViewModelCreator = () -> ViewModel?
-
-class ViewModelFactory(
- private val viewModelCreator: ViewModelCreator = { null },
-) : ViewModelProvider.Factory {
-
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T = viewModelCreator() as T
-}
-
-@Composable
-inline fun viewModelCreator(noinline creator: ViewModelCreator): VM =
- viewModel(factory = remember { ViewModelFactory(creator) })
-
-private const val RESPONSE_DELAY = 1500L
\ No newline at end of file
+ ProvideDynamicPreview(
+ moduleDeclaration = {
+ single { mockUseCase }
+ single { CreateQuizScreenMapper() }
+ viewModel {
+ CreateQuizViewModel(getSpecializationsListUseCase = get(), screenMapper = get())
+ }
+ },
+ content = {
+ val previewViewModel = koinViewModel()
+ val previewState = previewViewModel.screenState.collectAsStateWithLifecycle()
+
+ ScreenUI(
+ state = previewState,
+ onEvent = previewViewModel::onEvent,
+ titleTopAppBar = previewState.value.titleTopAppBar
+ )
+ }
+ )
+}
\ No newline at end of file
From c0dbbe19002ecb54683ba87ff66088fcecc52be3 Mon Sep 17 00:00:00 2001
From: Deyryl <62314001+Deyryl@users.noreply.github.com>
Date: Mon, 1 Jun 2026 19:00:29 +0300
Subject: [PATCH 24/28] =?UTF-8?q?[=D0=9F=D1=83=D0=B1=D0=BB=D0=B8=D1=87?=
=?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D0=BD=D0=B0=D0=B6=D0=B5?=
=?UTF-8?q?=D1=80]=20ANDR-72:=20=D0=A7=D0=B5=D1=82=D0=B2=D0=B5=D1=80=D1=82?=
=?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF=20InterviewQuizScreen.?=
=?UTF-8?q?=20=D0=9D=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D1=8F=20+?=
=?UTF-8?q?=20=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F?=
=?UTF-8?q?=20(#150)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ANDR-72: настройка DI, исправление минорных ошибок, добавление QuizAnswersWrapperDto
* ANDR-72: удаление лишней строки
* ANDR-72: удаление лишнего импорта
---
app/src/main/java/ru/yeahub/Application.kt | 2 ++
.../models/GetNewMockQuizResponse.kt | 2 +-
.../network_api/models/QuestionAnswerDto.kt | 2 +-
.../models/QuizAnswersWrapperDto.kt | 5 +++
.../yeahub/network_impl/RetrofitApiService.kt | 2 +-
.../interviewQuiz/di/InterviewQuizModule.kt | 36 +++++++++++++++++++
.../presentation/InterviewQuizViewModel.kt | 2 +-
.../interviewQuiz/ui/InterviewQuizScreen.kt | 2 +-
.../InterviewQuizDataToDomainMapperTest.kt | 3 +-
9 files changed, 50 insertions(+), 6 deletions(-)
create mode 100644 core/network-api/src/main/java/ru/yeahub/network_api/models/QuizAnswersWrapperDto.kt
create mode 100644 feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/di/InterviewQuizModule.kt
diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt
index 8d2a0b8d..99059246 100644
--- a/app/src/main/java/ru/yeahub/Application.kt
+++ b/app/src/main/java/ru/yeahub/Application.kt
@@ -8,6 +8,7 @@ import ru.yeahub.example_details.impl.detailsFeatureModule
import ru.yeahub.example_home.impl.data.di.questionsMainFeatureModule
import ru.yeahub.example_profile.impl.profileFeatureModule
import ru.yeahub.interview_trainer.impl.createQuiz.di.createQuizModule
+import ru.yeahub.interview_trainer.impl.interviewQuiz.di.interviewQuizModule
import ru.yeahub.interview_trainer.impl.interviewQuizResult.di.interviewQuizResultModule
import ru.yeahub.navigation_impl.navigationPathModule
import ru.yeahub.network_impl.networkModule
@@ -57,6 +58,7 @@ class Application : Application() {
collectionsAndQuestionsFeatureModule,
specializationFeatureModule,
createQuizModule,
+ interviewQuizModule,
interviewQuizResultModule
)
}
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
index 6584f201..2d283972 100644
--- a/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/GetNewMockQuizResponse.kt
@@ -5,6 +5,6 @@ data class GetNewMockQuizResponse(
val startDate: String,
val fullCount: Int,
val skills: List,
- val response: List,
+ val response: QuizAnswersWrapperDto,
val questions: List
)
\ No newline at end of file
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
index ac8ed579..89c1f01b 100644
--- a/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuestionAnswerDto.kt
@@ -1,7 +1,7 @@
package ru.yeahub.network_api.models
data class QuestionAnswerDto(
- val questionId: Int,
+ val questionId: Long,
val questionTitle: String,
val answer: String
)
diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/QuizAnswersWrapperDto.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuizAnswersWrapperDto.kt
new file mode 100644
index 00000000..c234fadc
--- /dev/null
+++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/QuizAnswersWrapperDto.kt
@@ -0,0 +1,5 @@
+package ru.yeahub.network_api.models
+
+data class QuizAnswersWrapperDto(
+ val answers: List
+)
diff --git a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
index dbfcc56c..28542684 100644
--- a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
+++ b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt
@@ -63,7 +63,7 @@ interface RetrofitApiService : ApiService {
@Query("isFree") isFree: Boolean
): GetCollectionsResponse
- @GET("/interview-preparation/quizzes/mock/new")
+ @GET("interview-preparation/quizzes/mock/new")
override suspend fun getQuizMockQuestions(
@Query("skills") skills: List?,
@Query("complexity") complexity: List?,
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/di/InterviewQuizModule.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/di/InterviewQuizModule.kt
new file mode 100644
index 00000000..021795d4
--- /dev/null
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/di/InterviewQuizModule.kt
@@ -0,0 +1,36 @@
+package ru.yeahub.interview_trainer.impl.interviewQuiz.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import ru.yeahub.interview_trainer.impl.interviewQuiz.data.InterviewQuizDataToDomainMapper
+import ru.yeahub.interview_trainer.impl.interviewQuiz.data.InterviewQuizRepositoryImpl
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.GetQuestionsListUseCase
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.GetQuestionsListUseCaseImpl
+import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.InterviewQuizRepositoryApi
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizScreenMapper
+import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizViewModel
+
+val interviewQuizModule = module {
+ // Мапперы
+ single { InterviewQuizDataToDomainMapper() }
+ single { InterviewQuizScreenMapper() }
+
+ // Репозиторий
+ single {
+ InterviewQuizRepositoryImpl(networkProvider = get(), mapper = get())
+ }
+
+ // Юзкейс
+ single {
+ GetQuestionsListUseCaseImpl(repository = get())
+ }
+
+ // Вьюмодель экрана
+ viewModel { params ->
+ InterviewQuizViewModel(
+ savedStateHandle = params.get(),
+ screenMapper = get(),
+ getQuestionsListUseCase = get()
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
index af88a583..9001e668 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/presentation/InterviewQuizViewModel.kt
@@ -56,7 +56,7 @@ open class InterviewQuizViewModel(
selectedAnswer = userInput.selectedAnswer
)
}.catch { e ->
- screenMapper.getScreenState(e)
+ emit(screenMapper.getScreenState(e))
}.stateIn(
scope = viewModelScopeSafe,
started = SharingStarted.WhileSubscribed(TIME_TO_CLEAN_UP_RESOURCES),
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
index bde28292..dd1eac61 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/interviewQuiz/ui/InterviewQuizScreen.kt
@@ -366,7 +366,7 @@ private fun QuestionCard(
}
} else {
SecondaryButton(
- onClick = {},
+ onClick = onResultClick,
modifier = Modifier
.width(170.dp)
.height(48.dp)
diff --git a/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
index af02ea37..e75012bf 100644
--- a/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
+++ b/feature/interview-trainer/impl/src/test/java/test/InterviewQuizDataToDomainMapperTest.kt
@@ -9,6 +9,7 @@ import ru.yeahub.interview_trainer.impl.interviewQuiz.domain.DomainQuestionsList
import ru.yeahub.network_api.models.GetNewMockQuizResponse
import ru.yeahub.network_api.models.GetQuestionResponse
import ru.yeahub.network_api.models.NestedUserReferenceDto
+import ru.yeahub.network_api.models.QuizAnswersWrapperDto
import ru.yeahub.test.TestArgumentsProvider
class InterviewQuizDataToDomainMapperTest {
@@ -87,7 +88,7 @@ class InterviewQuizDataToDomainMapperTest {
startDate = "01.01.2026",
fullCount = 2,
skills = emptyList(),
- response = emptyList(),
+ response = QuizAnswersWrapperDto(emptyList()),
questions = listOf(defaultQuestionResponse, defaultQuestionResponseWithLongAnswer),
)
From 5ee005f3c4b54159a97fcbf314091d2ecb8b202d Mon Sep 17 00:00:00 2001
From: Jeskrill <107257292+Jeskrill@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:59:00 +0300
Subject: [PATCH 25/28] =?UTF-8?q?ANDR-104:=20=D1=84=D0=B8=D1=87=D0=B0-?=
=?UTF-8?q?=D1=82=D0=BE=D0=B3=D0=B3=D0=BB=20EnableInterviewTrainer?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../interview_trainer/api/EnableInterviewTrainer.kt | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/EnableInterviewTrainer.kt
diff --git a/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/EnableInterviewTrainer.kt b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/EnableInterviewTrainer.kt
new file mode 100644
index 00000000..599b6393
--- /dev/null
+++ b/feature/interview-trainer/api/src/main/java/ru/yeahub/interview_trainer/api/EnableInterviewTrainer.kt
@@ -0,0 +1,9 @@
+package ru.yeahub.interview_trainer.api
+
+import ru.yeahub.feature_toggle_api.FeatureToggle
+
+object EnableInterviewTrainer : FeatureToggle(
+ key = "enable_interview_trainer",
+ defaultValue = false,
+ description = "Доступность фичи Interview Trainer"
+)
From 629787e2e50d170a093f5b9330d8a52192ac762e Mon Sep 17 00:00:00 2001
From: Jeskrill <107257292+Jeskrill@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:59:37 +0300
Subject: [PATCH 26/28] =?UTF-8?q?ANDR-104:=20=D0=B3=D0=B5=D0=B9=D1=82?=
=?UTF-8?q?=D0=B8=D0=BD=D0=B3=20Interview=20Trainer=20=D0=BF=D0=BE=20?=
=?UTF-8?q?=D1=84=D0=B8=D1=87=D0=B0-=D1=82=D0=BE=D0=B3=D0=B3=D0=BB=D1=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../interview_trainer/impl/InterviewTrainerFeatureImpl.kt | 4 ++++
.../interview_trainer/impl/createQuiz/di/CreateQuizModule.kt | 4 ++++
2 files changed, 8 insertions(+)
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
index 28eb28cb..1d512ead 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/InterviewTrainerFeatureImpl.kt
@@ -6,6 +6,8 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
+import ru.yeahub.feature_toggle_api.FeatureToggle
+import ru.yeahub.interview_trainer.api.EnableInterviewTrainer
import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizResult
import ru.yeahub.interview_trainer.impl.createQuiz.ui.CreateQuizScreen
import ru.yeahub.interview_trainer.impl.interviewQuiz.presentation.InterviewQuizResult
@@ -23,6 +25,8 @@ private const val QUIZ_ANSWERS_KEY = "quizAnswersKey"
class InterviewTrainerFeatureImpl : FeatureApi {
override fun getFeatureName(): String = FeatureRoute.InterviewTrainerFeature.FEATURE_NAME
+ override fun featureToggle(): FeatureToggle = EnableInterviewTrainer
+
override fun registerGraph(
navGraphBuilder: NavGraphBuilder,
navController: NavHostController,
diff --git a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
index 65022d24..b4d8155e 100644
--- a/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
+++ b/feature/interview-trainer/impl/src/main/java/ru/yeahub/interview_trainer/impl/createQuiz/di/CreateQuizModule.kt
@@ -3,6 +3,8 @@ package ru.yeahub.interview_trainer.impl.createQuiz.di
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module
+import ru.yeahub.feature_toggle_api.registerFeatureToggle
+import ru.yeahub.interview_trainer.api.EnableInterviewTrainer
import ru.yeahub.interview_trainer.impl.InterviewTrainerFeatureImpl
import ru.yeahub.interview_trainer.impl.createQuiz.data.CreateQuizDataToDomainMapper
import ru.yeahub.interview_trainer.impl.createQuiz.data.CreateQuizRepositoryImpl
@@ -14,6 +16,8 @@ import ru.yeahub.interview_trainer.impl.createQuiz.presentation.CreateQuizViewMo
import ru.yeahub.navigation_api.FeatureApi
val createQuizModule = module {
+ registerFeatureToggle(EnableInterviewTrainer)
+
// FeatureImpl для реализации навигации фичи
single { InterviewTrainerFeatureImpl() } bind FeatureApi::class
From 4fd974ef93c036f607c7feb16f885fcdd110a8cf Mon Sep 17 00:00:00 2001
From: Jeskrill <107257292+Jeskrill@users.noreply.github.com>
Date: Mon, 29 Jun 2026 17:01:55 +0300
Subject: [PATCH 27/28] =?UTF-8?q?ANDR-104:=20=D1=81=D0=BA=D1=80=D1=8B?=
=?UTF-8?q?=D1=82=D0=B8=D0=B5=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?=
=?UTF-8?q?=D0=B8=20Interview=20Trainer=20=D0=BD=D0=B0=20home?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../data/di/QuestionsMainViewModelModule.kt | 3 ++-
.../mapper/QuestionMainScreenMapper.kt | 24 ++++++++++++-------
.../viewmodel/QuestionMainViewModel.kt | 19 ++++++++-------
3 files changed, 28 insertions(+), 18 deletions(-)
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/data/di/QuestionsMainViewModelModule.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/data/di/QuestionsMainViewModelModule.kt
index 1e9a221c..9e1556b5 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/data/di/QuestionsMainViewModelModule.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/data/di/QuestionsMainViewModelModule.kt
@@ -7,7 +7,8 @@ import ru.yeahub.example_home.impl.presentation.viewmodel.QuestionMainViewModel
val questionsViewModelModule = module {
viewModel {
QuestionMainViewModel(
- domainMapper = get()
+ domainMapper = get(),
+ featureAvailabilityService = get()
)
}
}
\ No newline at end of file
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
index 5e435fbf..1828e0e1 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapper.kt
@@ -6,21 +6,27 @@ import ru.yeahub.example_home.impl.presentation.model.QuestionMainUiModel
import ru.yeahub.ui.R
class QuestionMainScreenMapper {
- fun getInitialUiModels(): List {
- return listOf(
+ fun getInitialUiModels(isInterviewTrainerEnabled: Boolean): List = buildList {
+ add(
QuestionMainUiModel(
type = QuestionMainItemType.BaseQuestions,
title = TextOrResource.Resource(R.string.base_questions_title),
description = TextOrResource.Resource(R.string.base_questions_description),
imageRes = R.drawable.icon_base_question
- ),
+ )
+ )
+ if (isInterviewTrainerEnabled) {
// TODO("imageRes тренажера потом изменить на нормальный")
- QuestionMainUiModel(
- type = QuestionMainItemType.InterviewTrainer,
- title = TextOrResource.Resource(R.string.interview_trainer_title),
- description = TextOrResource.Resource(R.string.interview_trainer_description),
- imageRes = R.drawable.question_square
- ),
+ add(
+ QuestionMainUiModel(
+ type = QuestionMainItemType.InterviewTrainer,
+ title = TextOrResource.Resource(R.string.interview_trainer_title),
+ description = TextOrResource.Resource(R.string.interview_trainer_description),
+ imageRes = R.drawable.question_square
+ )
+ )
+ }
+ add(
QuestionMainUiModel(
type = QuestionMainItemType.Collections,
title = TextOrResource.Resource(R.string.collections_title),
diff --git a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
index ba3b7c6a..a94d2a07 100644
--- a/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
+++ b/feature/example-home/impl/src/main/java/ru/yeahub/example_home/impl/presentation/viewmodel/QuestionMainViewModel.kt
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import ru.yeahub.example_home.impl.presentation.intents.QuestionMainScreenCommand
@@ -14,21 +14,24 @@ import ru.yeahub.example_home.impl.presentation.intents.QuestionMainScreenEvent
import ru.yeahub.example_home.impl.presentation.mapper.QuestionMainScreenMapper
import ru.yeahub.example_home.impl.presentation.model.QuestionMainItemType
import ru.yeahub.example_home.impl.presentation.state.QuestionMainScreenState
+import ru.yeahub.feature_toggle_api.FeatureAvailabilityService
+import ru.yeahub.interview_trainer.api.EnableInterviewTrainer
class QuestionMainViewModel(
private val domainMapper: QuestionMainScreenMapper,
+ private val featureAvailabilityService: FeatureAvailabilityService,
) : ViewModel() {
private val _command = MutableSharedFlow()
val command: SharedFlow = _command
- private val initialStateFlow = flow {
- emit(QuestionMainScreenState.Loading)
- val uiModels = domainMapper.getInitialUiModels()
- emit(QuestionMainScreenState.Content(uiModels))
- }
-
- val state: StateFlow = initialStateFlow
+ val state: StateFlow = featureAvailabilityService.featureFlagsSnapshot
+ .map {
+ val uiModels = domainMapper.getInitialUiModels(
+ isInterviewTrainerEnabled = featureAvailabilityService.isFeatureEnabled(EnableInterviewTrainer)
+ )
+ QuestionMainScreenState.Content(uiModels)
+ }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
From 3e0a41c7a5f792243fe795dd9e579dff52eeb532 Mon Sep 17 00:00:00 2001
From: Jeskrill <107257292+Jeskrill@users.noreply.github.com>
Date: Mon, 29 Jun 2026 17:02:21 +0300
Subject: [PATCH 28/28] =?UTF-8?q?ANDR-104:=20=D0=A2=D0=B5=D1=81=D1=82=20?=
=?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?=
=?UTF-8?q?=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
feature/example-home/impl/build.gradle.kts | 11 +++-
.../mapper/QuestionMainScreenMapperTest.kt | 56 +++++++++++++++++++
2 files changed, 66 insertions(+), 1 deletion(-)
create mode 100644 feature/example-home/impl/src/test/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapperTest.kt
diff --git a/feature/example-home/impl/build.gradle.kts b/feature/example-home/impl/build.gradle.kts
index 4afcd8a4..b42a561c 100644
--- a/feature/example-home/impl/build.gradle.kts
+++ b/feature/example-home/impl/build.gradle.kts
@@ -37,13 +37,20 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
+ testOptions {
+ unitTests.all {
+ it.useJUnitPlatform()
+ }
+ }
}
dependencies {
implementation(project(":core:ui"))
implementation(project(":core:utils"))
implementation(project(":core:navigation-api"))
+ implementation(project(":core:feature-toggle-api"))
implementation(project(":feature:example-home:api"))
+ implementation(project(":feature:interview-trainer:api"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
@@ -63,7 +70,9 @@ dependencies {
// Timber
implementation(libs.timber)
- testImplementation(libs.junit)
+ testImplementation(libs.junit.jupiter)
+ testImplementation(platform(libs.junit.bom))
+ testRuntimeOnly(libs.junit.platform.launcher)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/feature/example-home/impl/src/test/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapperTest.kt b/feature/example-home/impl/src/test/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapperTest.kt
new file mode 100644
index 00000000..906c26ee
--- /dev/null
+++ b/feature/example-home/impl/src/test/java/ru/yeahub/example_home/impl/presentation/mapper/QuestionMainScreenMapperTest.kt
@@ -0,0 +1,56 @@
+package ru.yeahub.example_home.impl.presentation.mapper
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.example_home.impl.presentation.model.QuestionMainItemType
+import java.util.stream.Stream
+
+class QuestionMainScreenMapperTest {
+
+ private val mapper = QuestionMainScreenMapper()
+
+ @ParameterizedTest
+ @ArgumentsSource(InterviewTrainerVisibilityArgumentsProvider::class)
+ fun `interview trainer card visibility follows feature toggle`(
+ testCase: InterviewTrainerVisibilityTestCase
+ ) {
+ val itemTypes = mapper.getInitialUiModels(
+ isInterviewTrainerEnabled = testCase.isInterviewTrainerEnabled
+ ).map { it.type }
+
+ Assertions.assertEquals(
+ testCase.expectedContainsInterviewTrainer,
+ itemTypes.contains(QuestionMainItemType.InterviewTrainer)
+ )
+ Assertions.assertTrue(itemTypes.contains(QuestionMainItemType.BaseQuestions))
+ Assertions.assertTrue(itemTypes.contains(QuestionMainItemType.Collections))
+ }
+
+ data class InterviewTrainerVisibilityTestCase(
+ val isInterviewTrainerEnabled: Boolean,
+ val expectedContainsInterviewTrainer: Boolean
+ )
+
+ class InterviewTrainerVisibilityArgumentsProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return Stream.of(
+ Arguments.of(
+ InterviewTrainerVisibilityTestCase(
+ isInterviewTrainerEnabled = true,
+ expectedContainsInterviewTrainer = true
+ )
+ ),
+ Arguments.of(
+ InterviewTrainerVisibilityTestCase(
+ isInterviewTrainerEnabled = false,
+ expectedContainsInterviewTrainer = false
+ )
+ )
+ )
+ }
+ }
+}