kotlin poezdec

This commit is contained in:
2025-11-30 20:26:32 +06:00
parent 01cf2c1439
commit 523c373a04
10 changed files with 764 additions and 87 deletions

View File

@@ -0,0 +1,76 @@
kotlin version: 2.0.21
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing C:/Users/Admin/AndroidStudioProjects/NTO-2025-Android-TeamTask/app/src/main/java/ru/myitschool/work/App.kt:7:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 33 more

View File

@@ -0,0 +1,76 @@
kotlin version: 2.0.21
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing C:/Users/Admin/AndroidStudioProjects/NTO-2025-Android-TeamTask/app/src/main/java/ru/myitschool/work/App.kt:7:5: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalArgumentException: source must not be null
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 33 more

View File

@@ -1,9 +1,3 @@
/*plugins {
composeCompiler
kotlinAndroid
kotlinSerialization version Version.Kotlin.language
androidApplication
}*/
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.data.repo
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore
// DataStore для хранения auth-кода
val Context.authDataStore by preferencesDataStore(name = "auth_data")

View File

@@ -1,6 +1,9 @@
package ru.myitschool.work.data.repo
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import ru.myitschool.work.App
import ru.myitschool.work.data.source.NetworkDataSource
@@ -10,23 +13,17 @@ object AuthRepository {
private val KEY_CODE = stringPreferencesKey("auth_code")
private var codeCache: String? = null
// suspend fun checkAndSave(text: String): Result<Boolean> {
// return NetworkDataSource.checkAuth(text).onSuccess { success ->
// if (success) {
// codeCache = text
// }
// }
// }
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
dataStore.edit { prefs ->
prefs[KEY_CODE] = text
// Проверка кода на сервере и сохранение при успехе
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
dataStore.edit { prefs ->
prefs[KEY_CODE] = text
}
}
}
}
}
/** Сохранённый код (из кэша или DataStore) */
suspend fun getSavedCode(): String? {
@@ -47,6 +44,4 @@ suspend fun checkAndSave(text: String): Result<Boolean> {
prefs.clear()
}
}
}
}

View File

@@ -19,6 +19,11 @@ import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.source.dto.UserDto
object NetworkDataSource {
/** Поставь false, когда поднимешь настоящий бэкенд */
private const val USE_STUB = true
// Реальный клиент Ktor (для будущего)
private val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
@@ -34,36 +39,78 @@ object NetworkDataSource {
}
}
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
// ----------------- Заглушечные данные -----------------
private val stubUser = UserDto(
name = "Тестовый пользователь",
booking = listOf(
UserDto.BookingDto(
room = "Опенспейс 1",
time = "2025-01-05"
),
UserDto.BookingDto(
room = "Опенспейс 2",
time = "2025-01-06"
),
)
)
private val stubAvailableBookings: List<UserDto.BookingDto> = listOf(
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-10"),
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-11"),
UserDto.BookingDto(room = "Опенспейс 2", time = "2025-01-10"),
UserDto.BookingDto(room = "Опенспейс 3", time = "2025-01-12"),
)
// ----------------- Публичные методы -----------------
suspend fun checkAuth(code: String): Result<Boolean> {
if (USE_STUB) {
// Примитивная проверка заглушки: 4 символа → ок
return Result.success(code.length == 4)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
}
}
}
}
suspend fun getUserInfo(code: String): Result<UserDto> {
if (USE_STUB) {
return Result.success(stubUser)
}
suspend fun getUserInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<UserDto>()
else -> error(response.bodyAsText())
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<UserDto>()
else -> error(response.bodyAsText())
}
}
}
}
suspend fun getAvailableBookings(
code: String
): Result<List<UserDto.BookingDto>> = withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
else -> error(response.bodyAsText())
): Result<List<UserDto.BookingDto>> {
if (USE_STUB) {
return Result.success(stubAvailableBookings)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
else -> error(response.bodyAsText())
}
}
}
}
@@ -72,24 +119,32 @@ object NetworkDataSource {
code: String,
room: String,
time: String
): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
setBody(
MultiPartFormDataContent(
formData {
append("room", room)
append("time", time)
}
): Result<Unit> {
if (USE_STUB) {
// Типа всё хорошо
return Result.success(Unit)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
setBody(
MultiPartFormDataContent(
formData {
append("room", room)
append("time", time)
}
)
)
)
}
when (response.status) {
HttpStatusCode.OK -> Unit
else -> error(response.bodyAsText())
}
when (response.status) {
HttpStatusCode.OK -> Unit
else -> error(response.bodyAsText())
}
}
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
}
private fun getUrl(code: String, targetUrl: String) =
"${Constants.HOST}/api/$code$targetUrl"
}

View File

@@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -15,6 +12,8 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
@Composable
fun AppNavHost(
@@ -32,18 +31,10 @@ fun AppNavHost(
AuthScreen(navController = navController)
}
composable<MainScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
MainScreen(navController = navController)
}
composable<BookScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
BookScreen(navController = navController)
}
}
}
}

View File

@@ -16,9 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -41,7 +38,10 @@ fun AuthScreen(
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
navController.navigate(MainScreenDestination) {
// очищаем backstack до экрана авторизации
popUpTo(MainScreenDestination) { inclusive = false }
}
}
}
@@ -60,6 +60,7 @@ fun AuthScreen(
when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> {
Spacer(modifier = Modifier.size(16.dp))
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
@@ -73,25 +74,41 @@ private fun Content(
viewModel: AuthViewModel,
state: AuthState.Data
) {
var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
if (state.isErrorVisible) {
Text(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Auth.ERROR),
text = "Неверный код или ошибка сервера",
color = Color.Red,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(8.dp))
}
TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
value = inputText,
onValueChange = {
inputText = it
viewModel.onIntent(AuthIntent.TextInput(it))
},
modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = state.code,
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
label = { Text(stringResource(R.string.auth_label)) }
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = {
viewModel.onIntent(AuthIntent.Send(inputText))
viewModel.onIntent(AuthIntent.Send(state.code))
},
enabled = true
enabled = state.isButtonEnabled
) {
Text(stringResource(R.string.auth_sign_in))
}
}
}

View File

@@ -0,0 +1,268 @@
package ru.myitschool.work.ui.screen.book
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds
@Composable
fun BookScreen(
navController: NavController,
viewModel: BookViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(BookIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
BookViewModel.Action.CloseWithSuccess -> {
navController.popBackStack()
}
}
}
}
when (val current = state) {
is BookState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is BookState.Error -> {
BookErrorContent(
message = current.message,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Empty -> {
BookEmptyContent(
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Data -> {
BookDataContent(
state = current,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) },
onSelectDate = { viewModel.onIntent(BookIntent.SelectDate(it)) },
onSelectPlace = { viewModel.onIntent(BookIntent.SelectPlace(it)) },
onBook = { viewModel.onIntent(BookIntent.Book) }
)
}
}
}
@Composable
private fun BookErrorContent(
message: String,
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
}
}
@Composable
private fun BookEmptyContent(
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.EMPTY),
text = "Всё забронировано"
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
}
}
@Composable
private fun BookDataContent(
state: BookState.Data,
onBack: () -> Unit,
onRefresh: () -> Unit,
onSelectDate: (Int) -> Unit,
onSelectPlace: (Int) -> Unit,
onBook: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
}
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
}
Spacer(modifier = Modifier.size(16.dp))
// Вкладки с датами
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.dates) { index, dateLabel ->
Surface(
modifier = Modifier
.testTag(TestIds.Book.getIdDateItemByPosition(index)),
tonalElevation = if (index == state.selectedDateIndex) 4.dp else 0.dp
) {
Text(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp)
.testTag(TestIds.Book.ITEM_DATE),
text = dateLabel,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
// Список мест
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.places, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
.selectable(
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
) {
Row(
modifier = Modifier
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT),
text = "${item.roomName}${item.time}",
style = MaterialTheme.typography.bodyMedium
)
}
RadioButton(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR),
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
}
}
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.BOOK_BUTTON),
onClick = onBook,
enabled = state.places.any { it.isSelected }
) {
Text("Забронировать")
}
}
}

View File

@@ -0,0 +1,198 @@
package ru.myitschool.work.ui.screen.main
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
@Composable
fun MainScreen(
navController: NavController,
viewModel: MainViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(MainIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
MainViewModel.Action.NavigateToAuth -> {
navController.navigate(AuthScreenDestination) {
popUpTo(AuthScreenDestination) { inclusive = true }
}
}
MainViewModel.Action.NavigateToBooking -> {
navController.navigate(BookScreenDestination)
}
}
}
}
when (val current = state) {
is MainState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is MainState.Error -> {
MainErrorContent(
message = current.message,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) }
)
}
is MainState.Data -> {
MainDataContent(
state = current,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) },
onLogout = { viewModel.onIntent(MainIntent.Logout) },
onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) }
)
}
}
}
@Composable
private fun MainErrorContent(
message: String,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
}
}
@Composable
private fun MainDataContent(
state: MainState.Data,
onRefresh: () -> Unit,
onLogout: () -> Unit,
onAddBooking: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE),
contentAlignment = Alignment.Center
) {
Text("🙂")
}
Spacer(modifier = Modifier.size(16.dp))
Text(
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME),
text = state.name,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.size(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON),
onClick = onLogout
) {
Text("Выход")
}
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Button(
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
onClick = onAddBooking
) {
Text("Бронь")
}
}
Spacer(modifier = Modifier.size(16.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.bookings, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.getIdItemByPosition(index))
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
text = item.dateLabel,
style = MaterialTheme.typography.bodyMedium
)
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
text = item.roomName,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}