Initial commit
Some checks failed
Merge core/template-android-project to this repo / merge-if-needed (push) Has been cancelled

This commit is contained in:
2025-11-04 19:02:59 +03:00
parent ac5d12ac53
commit 47361868d8
21 changed files with 423 additions and 455 deletions

View File

@@ -36,10 +36,5 @@ android {
dependencies {
defaultComposeLibrary()
val ktor = "3.3.1"
implementation("io.ktor:ktor-client-core:$ktor")
implementation("io.ktor:ktor-client-cio:$ktor")
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.compose.material:material-icons-extended:1.7.8")
}

View File

@@ -15,7 +15,7 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.root.RootActivity"
android:name=".ui.RootActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:label="@string/title_activity_root">

View File

@@ -1,9 +0,0 @@
package ru.myitschool.work.core
object Constants {
const val HOST = "http://10.0.2.2:8080"
const val USER_URL = "/user"
const val FULL_USER_URL = "$HOST$USER_URL"
const val BOOK_URL = "/book"
const val FULL_BOOK_URL = "$HOST$BOOK_URL"
}

View File

@@ -0,0 +1,13 @@
package ru.myitschool.work.core
object TestIds {
const val AUTH_TITLE = "auth_title"
const val AUTH_INPUT_EMAIL = "auth_input_email"
const val AUTH_INPUT_PASSWORD = "auth_input_password"
const val AUTH_INPUT_PASSWORD_VISIBILITY = "auth_input_password_visibility"
const val AUTH_LOGIN = "auth_login"
const val AUTH_ERROR = "auth_error"
const val AUTH_FORGOT_PASS = "auth_forgot_pass"
const val AUTH_CREATE_ACCOUNT = "auth_create_account"
const val AUTH_SUCCESS = "auth_success"
}

View File

@@ -1,26 +0,0 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.entities.BookingEntity
import ru.myitschool.work.domain.entities.UserEntity
object AppRepository {
suspend fun loadData(): Result<UserEntity> {
return NetworkDataSource.getUser().map { dto ->
UserEntity(
name = dto.name,
bookingList = dto.booking.map { bookingDto ->
BookingEntity(
roomName = bookingDto.room,
time = bookingDto.time
)
}
)
}
}
suspend fun addBook(room: String, time: String): Result<Unit> {
return NetworkDataSource.addBook(room, time)
}
}

View File

@@ -1,10 +0,0 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ErrorDto(
@SerialName("error")
val error: String,
)

View File

@@ -1,20 +0,0 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserDto(
@SerialName("name")
val name: String,
@SerialName("booking")
val booking: List<BookingDto>
) {
@Serializable
data class BookingDto(
@SerialName("room")
val room: String,
@SerialName("time")
val time: String,
)
}

View File

@@ -1,71 +0,0 @@
package ru.myitschool.work.data.source
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.forms.FormPart
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.ErrorDto
import ru.myitschool.work.data.dto.UserDto
object NetworkDataSource {
private val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
json(
Json {
isLenient = true
ignoreUnknownKeys = true
explicitNulls = true
encodeDefaults = true
}
)
}
}
}
suspend fun getUser(): Result<UserDto> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(Constants.FULL_USER_URL)
if (response.status == HttpStatusCode.OK) {
response.body<UserDto>()
} else {
error(response.body<ErrorDto>().error)
}
}
}
suspend fun addBook(
room: String,
time: String,
): Result<Unit> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.post(Constants.FULL_BOOK_URL) {
setBody(
MultiPartFormDataContent(
formData(
FormPart("room", room),
FormPart("time", time),
)
)
)
}
if (response.status == HttpStatusCode.OK) {
Unit
} else {
error(response.body<ErrorDto>().error)
}
}
}
}

View File

@@ -1,17 +0,0 @@
package ru.myitschool.work.domain
import ru.myitschool.work.data.AppRepository
class AddBookUseCase(
private val repository: AppRepository
) {
suspend operator fun invoke(
room: String,
time: String,
): Result<Unit> {
return repository.addBook(
room = room,
time = time,
)
}
}

View File

@@ -1,12 +0,0 @@
package ru.myitschool.work.domain
import ru.myitschool.work.data.AppRepository
import ru.myitschool.work.domain.entities.UserEntity
class GetUserDataUseCase(
private val repository: AppRepository
) {
suspend operator fun invoke(): Result<UserEntity> {
return repository.loadData()
}
}

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.domain.entities
data class BookingEntity(
val roomName: String,
val time: String,
)

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.domain.entities
data class UserEntity(
val name: String,
val bookingList: List<BookingEntity>
)

View File

@@ -0,0 +1,58 @@
package ru.myitschool.work.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import ru.myitschool.work.ui.auth.AuthScreen
import ru.myitschool.work.ui.auth.AuthViewModel
import ru.myitschool.work.ui.theme.WorkTheme
/**
* Этот файл не меняем!
*
* Здесь уже подключена ViewModel, которая управляет логикой состояний:
* 1) uiState: текущее состояние (Default, Loading, Error, Success)
* 2) email/password: тексты из полей
* 3) isButtonEnabled: доступность кнопки "Войти"
*
* ВАЖНО: Дизайнерам редактировать только функцию AuthScreen().
* Всё остальное — готовая логика, менять ничего не нужно.
*/
class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
WorkTheme {
Scaffold(modifier = Modifier.Companion.fillMaxSize()) { innerPadding ->
val viewModel: AuthViewModel = viewModel()
val uiState = viewModel.uiState.collectAsState().value
val email = viewModel.email.collectAsState().value
val password = viewModel.password.collectAsState().value
val isButtonEnabled = viewModel.isButtonEnabled.collectAsState().value
AuthScreen(
modifier = Modifier.Companion
.fillMaxSize()
.padding(innerPadding),
uiState = uiState,
email = email,
password = password,
isButtonEnabled = isButtonEnabled,
onEmailChange = viewModel::onEmailChange,
onPasswordChange = viewModel::onPasswordChange,
onLoginClick = viewModel::onLoginClick,
onForgotPasswordClick = viewModel::onForgotPasswordClick,
onCreateAccountClick = viewModel::onCreateAccountClick
)
}
}
}
}
}

View File

@@ -0,0 +1,213 @@
package ru.myitschool.work.ui.auth
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
/**
* Дизайнеры реализуют UI в функции AuthContent.
*
* Входные параметры:
* - uiState — текущее состояние экрана (Default / Loading / Error / Success)
* - email / password — тексты, которые сейчас в полях
* - isButtonEnabled — нужно визуально показать активность кнопки
*
* Колбэки:
* - onEmailChange() — вызывается при изменении email
* - onPasswordChange() — вызывается при изменении пароля
* - onLoginClick() — вызывается при нажатии кнопки "Войти"
* - onForgotPasswordClick() — "Забыли пароль?"
* - onCreateAccountClick() — "Создать аккаунт"
*
* ВАЖНО:
* - Никакой логики добавлять не нужно!
* - Только визуальное оформление Compose по заданию олимпиады.
*/
@Composable
fun AuthScreen(
modifier: Modifier = Modifier,
uiState: AuthUiState,
email: String,
password: String,
isButtonEnabled: Boolean,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit,
onForgotPasswordClick: () -> Unit,
onCreateAccountClick: () -> Unit,
) {
Box(modifier = modifier) {
when (uiState) {
is AuthUiState.Default,
is AuthUiState.Error -> {
// Форма видна и в Default, и при ошибке
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.Top
) {
Text(
modifier = Modifier
.padding(bottom = 16.dp)
.testTag(TestIds.AUTH_TITLE),
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground,
)
// Email
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.AUTH_INPUT_EMAIL),
value = email,
onValueChange = onEmailChange,
label = { Text(stringResource(R.string.auth_input_email_title)) },
placeholder = { Text(stringResource(R.string.auth_input_email_placeholder)) },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
isError = uiState is AuthUiState.Error
)
Spacer(modifier = Modifier.height(4.dp))
// Пароль
var passwordVisible by remember { mutableStateOf(false) }
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.AUTH_INPUT_PASSWORD),
value = password,
onValueChange = onPasswordChange,
label = { Text(stringResource(R.string.auth_input_password_title)) },
placeholder = { Text(stringResource(R.string.auth_input_password_placeholder)) },
singleLine = true,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
modifier = Modifier
.testTag(TestIds.AUTH_INPUT_PASSWORD_VISIBILITY),
onClick = { passwordVisible = !passwordVisible }
) {
Icon(
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
contentDescription = stringResource(
if (passwordVisible) {
R.string.auth_input_password_visibility_hide
} else {
R.string.auth_input_password_visibility_show
}
)
)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
isError = uiState is AuthUiState.Error
)
Spacer(modifier = Modifier.height(4.dp))
if (uiState is AuthUiState.Error) {
Text(
modifier = Modifier
.padding(start = 16.dp, bottom = 8.dp)
.testTag(TestIds.AUTH_ERROR),
text = stringResource(R.string.auth_error),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium,
)
}
Spacer(modifier = Modifier.height(16.dp))
// Кнопка Войти
Button(
onClick = onLoginClick,
enabled = isButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.testTag(TestIds.AUTH_LOGIN)
) {
Text(stringResource(R.string.auth_login))
}
Spacer(modifier = Modifier.height(16.dp))
// Ссылки
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(
modifier = Modifier.testTag(TestIds.AUTH_FORGOT_PASS),
onClick = onForgotPasswordClick
) {
Text(stringResource(R.string.auth_forgot_pass))
}
TextButton(
modifier = Modifier.testTag(TestIds.AUTH_CREATE_ACCOUNT),
onClick = onCreateAccountClick
) {
Text(stringResource(R.string.auth_create_account))
}
}
}
}
is AuthUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is AuthUiState.Success -> {
Text(
text = stringResource(R.string.auth_success),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Center)
.testTag(TestIds.AUTH_SUCCESS),
)
}
}
}
}

View File

@@ -0,0 +1,26 @@
package ru.myitschool.work.ui.auth
/**
* Этот файл не меняем!
* AuthUiState — это "состояния" экрана авторизации.
*
* Что это значит:
* Экран может быть в одном из четырёх состояний,
* ViewModel (логика) будет переключать эти состояния,
* дизайнеры — отображать соответствующий интерфейс.
*
* Состояния:
* - Default -> Обычная форма входа (поля, кнопки, ссылки)
* - Loading -> Показать крутящийся индикатор (ожидание)
* - Error -> Показать сообщение об ошибке
* - Success -> Показать сообщение об успешном входе
*
* Задача дизайнеров — в зависимости от состояния uiState
* отрисовать нужный экран в Compose.
*/
sealed interface AuthUiState {
data object Default : AuthUiState
data object Loading : AuthUiState
data object Error : AuthUiState
data object Success : AuthUiState
}

View File

@@ -0,0 +1,98 @@
package ru.myitschool.work.ui.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Этот файл не меняем!
*
* AuthViewModel — это логика, которая управляет экраном авторизации.
*
* Дизайнерам важно понимать:
* - ViewModel не рисует интерфейс, она только "сообщает", что нужно показать.
* - Все нужные данные (email, пароль, состояние экрана, активность кнопки)
* передаются в ваш Composable через параметры.
* - Вам не нужно ничего здесь менять.
*
* Основная идея:
* - Пользователь вводит email и пароль.
* - ViewModel проверяет, можно ли активировать кнопку "Войти".
* - При нажатии "Войти" ViewModel меняет состояние на Loading, потом Success или Error.
*/
class AuthViewModel : ViewModel() {
// uiState — текущее состояние экрана (Default / Loading / Error / Success)
private val _uiState = MutableStateFlow<AuthUiState>(AuthUiState.Default)
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
// email и password — значения, введённые пользователем в поля ввода
private val _email = MutableStateFlow("")
val email: StateFlow<String> = _email.asStateFlow()
private val _password = MutableStateFlow("")
val password: StateFlow<String> = _password.asStateFlow()
// isButtonEnabled — активна ли кнопка "Войти"
// (true, если оба поля не пустые)
private val _isButtonEnabled = MutableStateFlow(false)
val isButtonEnabled: StateFlow<Boolean> = _isButtonEnabled.asStateFlow()
/**
* Функции onEmailChange / onPasswordChange
* вызываются каждый раз, когда пользователь что-то вводит в поля.
* Они обновляют значения email / passwor
* и проверяют, можно ли активировать кнопку.
*/
fun onEmailChange(newEmail: String) {
_email.value = newEmail
validate()
}
fun onPasswordChange(newPassword: String) {
_password.value = newPassword
validate()
}
/**
* Проверка, можно ли нажимать кнопку "Войти".
* Если оба поля не пустые → isButtonEnabled = true
*/
private fun validate() {
_isButtonEnabled.value = _email.value.isNotBlank() && _password.value.isNotBlank()
}
/**
* При нажатии на кнопку "Войти"
* ViewModel запускает имитацию сетевого запроса:
* 1. Меняет состояние на Loading -> дизайнеры должны показать индикатор.
* 2. Через 1.5 секунды:
* - Если логин "user@example.com" и пароль "1234" -> Success
* - Иначе → Error
*
* Это имитация. В реальном приложении здесь будет запрос на сервер.
*/
fun onLoginClick() {
_uiState.value = AuthUiState.Loading
// имитация запроса — здесь только логика, без дизайна
viewModelScope.launch {
delay(1500)
_uiState.value = if (_email.value == "user@example.com" && _password.value == "1234") {
AuthUiState.Success
} else {
AuthUiState.Error
}
}
}
fun resetToDefault() {
_uiState.value = AuthUiState.Default
}
fun onForgotPasswordClick() { /* TODO */ }
fun onCreateAccountClick() { /* TODO */ }
}

View File

@@ -1,181 +0,0 @@
package ru.myitschool.work.ui.root
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.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.items
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import ru.myitschool.work.ui.theme.WorkTheme
class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
WorkTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Screen(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Screen(
modifier: Modifier = Modifier,
viewModel: RootViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
when (val currentState = state) {
is RootState.Content -> {
Column(
modifier = modifier,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = currentState.userEntity.name,
style = MaterialTheme.typography.headlineSmall
)
ButtonGetData(viewModel)
}
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(currentState.userEntity.bookingList) { book ->
Row(
modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = book.roomName,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = book.time,
style = MaterialTheme.typography.bodySmall
)
}
}
}
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var roomText by remember { mutableStateOf("") }
var timeText by remember { mutableStateOf("") }
Column(modifier = Modifier.weight(1f)) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = roomText,
onValueChange = { roomText = it },
label = { Text("Room") }
)
TextField(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
value = timeText,
onValueChange = { timeText = it },
label = { Text("Time") }
)
}
Column(
modifier = Modifier.padding(start = 8.dp),
) {
Button(
onClick = {
viewModel.onIntent(
RootIntent.AddBook(room = roomText, time = timeText)
)
roomText = ""
timeText = ""
}
) {
Text(text = "Add")
}
if (currentState.errorText != null) {
Text(text = currentState.errorText)
}
}
}
}
}
is RootState.Error -> {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = currentState.message)
ButtonGetData(viewModel)
}
}
is RootState.Loading -> {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
}
}
is RootState.NotLoaded -> {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
ButtonGetData(viewModel)
}
}
}
}
@Composable
private fun ButtonGetData(
viewModel: RootViewModel
) {
Button(onClick = { viewModel.onIntent(RootIntent.LoadData) }) {
Text(text = "Get load")
}
}

View File

@@ -1,9 +0,0 @@
package ru.myitschool.work.ui.root
sealed interface RootIntent {
data object LoadData: RootIntent
data class AddBook(
val room: String,
val time: String
): RootIntent
}

View File

@@ -1,13 +0,0 @@
package ru.myitschool.work.ui.root
import ru.myitschool.work.domain.entities.UserEntity
sealed interface RootState {
data object NotLoaded: RootState
data object Loading: RootState
data class Error(val message: String): RootState
data class Content(
val userEntity: UserEntity,
val errorText: String?,
): RootState
}

View File

@@ -1,68 +0,0 @@
package ru.myitschool.work.ui.root
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.AppRepository
import ru.myitschool.work.domain.AddBookUseCase
import ru.myitschool.work.domain.GetUserDataUseCase
class RootViewModel : ViewModel() {
private val getUserDataUseCase by lazy {
GetUserDataUseCase(
repository = AppRepository
)
}
private val addBookUseCase by lazy {
AddBookUseCase(
repository = AppRepository
)
}
private val _uiState = MutableStateFlow<RootState>(RootState.NotLoaded)
val uiState: StateFlow<RootState> = _uiState.asStateFlow()
fun onIntent(intent: RootIntent) {
when (intent) {
is RootIntent.LoadData -> loadData()
is RootIntent.AddBook -> addBook(intent)
}
}
private fun loadData() {
viewModelScope.launch {
_uiState.emit(RootState.Loading)
getUserDataUseCase.invoke().fold(
onSuccess = { value ->
_uiState.emit(RootState.Content(userEntity = value, errorText = null))
},
onFailure = { error ->
_uiState.emit(RootState.Error(error.message.orEmpty()))
}
)
}
}
private fun addBook(intent: RootIntent.AddBook) {
viewModelScope.launch {
addBookUseCase.invoke(
room = intent.room,
time = intent.time
).fold(
onSuccess = {
loadData()
},
onFailure = { error ->
_uiState.update { state ->
(state as? RootState.Content)?.copy(
errorText = error.message
) ?: state
}
}
)
}
}
}

View File

@@ -1,4 +1,17 @@
<resources>
<string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string>
<string name="auth_title">Вход в систему</string>
<string name="auth_input_email_title">Email</string>
<string name="auth_input_email_placeholder">Введите email</string>
<string name="auth_input_password_title">Пароль</string>
<string name="auth_input_password_placeholder">Введите пароль</string>
<string name="auth_input_password_visibility_show">Показать пароль</string>
<string name="auth_input_password_visibility_hide">Скрыть пароль</string>
<string name="auth_login">Войти</string>
<string name="auth_error">Ошибка: неверный логин или пароль</string>
<string name="auth_forgot_pass">Забыли пароль?</string>
<string name="auth_create_account">Создать аккаунт</string>
<string name="auth_success">Добро пожаловать!</string>
</resources>