Done UI part of LoginViewModel

TODO: Make DecoratedButton function
TODO: Domain level :<
This commit is contained in:
Nymos 2025-02-18 18:53:26 +03:00
parent 5a93965cb3
commit 35eb1c5b5e
4 changed files with 239 additions and 6 deletions

View File

@ -0,0 +1,195 @@
package com.nto.presentation.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nto.presentation.R
import com.nto.presentation.theme.BoxGray
import com.nto.presentation.theme.NTOTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Configuration for an [InputField].
*
* @param containerColor base background color. Overrides background color of textFieldColor and being used inside row's modifier.
* @param textFieldColors textFieldDefaults that are used for textField. Container and indicator colors are being overridden.
* @param paddingValues padding for text. Recommended value is 20.dp from start.
*
**/
@Immutable
class InputFieldOptions(
val containerColor: Color = BoxGray,
val textFieldColors: TextFieldColors? = null,
val paddingValues: PaddingValues = PaddingValues(start = 20.dp),
val isConfidential: Boolean = false
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is InputFieldOptions) return false
if (containerColor != other.containerColor) return false
if (textFieldColors != other.textFieldColors) return false
if (paddingValues != other.paddingValues) return false
return true
}
override fun hashCode(): Int {
var result = containerColor.hashCode()
result = 31 * result + textFieldColors.hashCode()
result = 31 * result + paddingValues.hashCode()
return result
}
}
/**
* High level element that uses Row with TextField to slightly improve final appearance.
*
* @param value mutable variable that represents current text. Should be in viewmodel or screen state.
* @param options instance of [InputFieldOptions] that contains background color, [PaddingValues] and [TextFieldColors] of element.
* @param modifier modifier that should contain [Modifier.height] and [Modifier.width] or other size definition to work correctly.
* @param shape row will be clipped to that shape.
* @param placeholder placeholder text to appear when no user input presents.
* @param onValueChange should contain an update function for [value].
*
* @sample InputFieldSample
*
*/
@Composable
fun InputField(
value: String,
modifier: Modifier = Modifier,
options: InputFieldOptions = InputFieldOptions(containerColor = NTOTheme.colors.inputFieldBackground),
shape: Shape = RoundedCornerShape(10.dp),
placeholder: String = "",
onValueChange: (String) -> Unit
) {
val containerColor = options.containerColor
val state = if (!options.isConfidential) null else {
remember { mutableStateOf(true) }
}
Row(
modifier = modifier.then(
Modifier
.clip(shape)
.background(containerColor)
), verticalAlignment = Alignment.CenterVertically
) {
TextField(value = value,
modifier = Modifier
.padding(options.paddingValues)
.fillMaxWidth(),
colors = (options.textFieldColors ?: TextFieldDefaults.colors()).copy(
unfocusedContainerColor = containerColor,
unfocusedIndicatorColor = Color.Transparent,
focusedContainerColor = containerColor,
focusedIndicatorColor = Color.Transparent,
),
onValueChange = onValueChange,
textStyle = NTOTheme.typography.displaySmall,
placeholder = {
Text(
placeholder,
style = NTOTheme.typography.displaySmall,
color = NTOTheme.colors.disabledText,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
},
trailingIcon = if (options.isConfidential) {
@Composable {
IconButton(modifier = Modifier.size(24.dp), onClick = {
state!!.value = !state.value
}) {
Icon(
painter = painterResource(if (!state!!.value) R.drawable.eye_invisible else R.drawable.eye_visible),
tint = if (value.isBlank()) NTOTheme.colors.disabledText else NTOTheme.colors.secondaryBackground,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
}
} else null,
visualTransformation = if (!options.isConfidential) VisualTransformation.None
else if (state!!.value) PasswordVisualTransformation()
else VisualTransformation.None)
}
}
@Stable
internal class InputFieldSample {
class SampleViewModel {
private val _state = MutableStateFlow(SampleState())
val state: StateFlow<SampleState>
get() = _state.asStateFlow()
fun setText(data: String) {
_state.value = _state.value.copy(data = data)
}
}
data class SampleState(
var data: String = ""
)
@Preview
@Composable
private fun InputFieldPreview() {
NTOTheme {
val sampleViewModel = SampleViewModel()
Surface(color = Color.White, modifier = Modifier.fillMaxSize()) {
Column {
InputField(
value = sampleViewModel.state.collectAsState().value.data,
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
placeholder = stringResource(R.string.placholder_email),
onValueChange = sampleViewModel::setText,
options = InputFieldOptions(isConfidential = true)
)
}
}
}
}
}

View File

@ -29,6 +29,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.nto.presentation.R import com.nto.presentation.R
import com.nto.presentation.composable.InputField
import com.nto.presentation.composable.InputFieldOptions
import com.nto.presentation.theme.NTOTheme import com.nto.presentation.theme.NTOTheme
@Composable @Composable
@ -37,7 +39,7 @@ fun LoginScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: LoginViewModel = hiltViewModel<LoginViewModel>(), viewModel: LoginViewModel = hiltViewModel<LoginViewModel>(),
) { ) {
val state = viewModel.state.collectAsState() val state = viewModel.state.collectAsState().value
Column( Column(
modifier = modifier.background(NTOTheme.colors.secondaryBackground), modifier = modifier.background(NTOTheme.colors.secondaryBackground),
@ -58,14 +60,14 @@ fun LoginScreen(
) )
} }
Text( Text(
text = "Вход", text = stringResource(R.string.greeting_login),
style = NTOTheme.typography.titleLarge, style = NTOTheme.typography.titleLarge,
color = NTOTheme.colors.secondaryText, color = NTOTheme.colors.secondaryText,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
Text( Text(
text = stringResource(R.string.greeting_login), text = stringResource(R.string.greeting_login_description),
style = NTOTheme.typography.displaySmall, style = NTOTheme.typography.displaySmall,
color = NTOTheme.colors.secondaryText, color = NTOTheme.colors.secondaryText,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@ -89,7 +91,11 @@ fun LoginScreen(
fontSize = 14.sp fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
//TODO: InputField InputField(
state.email,
placeholder = stringResource(R.string.placholder_email),
onValueChange = viewModel::setEmail
)
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
@ -100,7 +106,12 @@ fun LoginScreen(
fontSize = 14.sp fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
//TODO: InputField InputField(
state.password,
placeholder = stringResource(R.string.placeholder_password),
options = InputFieldOptions(isConfidential = true),
onValueChange = viewModel::setPassword
)
} }
Spacer(Modifier.height(50.dp)) Spacer(Modifier.height(50.dp))
//TODO: LoginButton //TODO: LoginButton

View File

@ -1,10 +1,14 @@
package com.nto.presentation.screens.loginScreen package com.nto.presentation.screens.loginScreen
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -13,4 +17,24 @@ class LoginViewModel @Inject constructor(): ViewModel(){
val state: StateFlow<LoginScreenState> val state: StateFlow<LoginScreenState>
get() = _state.asStateFlow() get() = _state.asStateFlow()
fun setEmail(value: String) {
_state.tryEmit(_state.value.copy(email = value))
checkInput()
}
fun setPassword(value: String) {
_state.tryEmit(_state.value.copy(password = value))
checkInput()
}
private fun checkInput() {
//TODO: domain level
}
fun login(navController: NavHostController) {
viewModelScope.launch(Dispatchers.IO) {
//TODO: domain level
}
}
} }

View File

@ -3,5 +3,8 @@
<string name="login_button">Войти</string> <string name="login_button">Войти</string>
<string name="text_email">Почта</string> <string name="text_email">Почта</string>
<string name="text_password">Пароль</string> <string name="text_password">Пароль</string>
<string name="greeting_login">Войдите в свой аккаунт чтобы продолжить</string> <string name="greeting_login_description">Войдите в свой аккаунт чтобы продолжить</string>
<string name="greeting_login">Вход</string>
<string name="placholder_email" translatable="false">example@mail.com</string>
<string name="placeholder_password" translatable="false">**********</string>
</resources> </resources>