simple login screen with simple viewmodel's logic

This commit is contained in:
2024-08-05 02:52:58 +03:00
parent 471af18f5e
commit 2c91f6bb62
11 changed files with 144 additions and 33 deletions
@@ -5,6 +5,7 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.FadeTransition
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
import dev.meloda.overseerr.theme.AppTheme
import org.koin.compose.KoinContext
@@ -13,7 +14,9 @@ import org.koin.compose.KoinContext
internal fun App() = KoinContext {
AppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Navigator(UrlScreen())
Navigator(UrlScreen()) { navigator ->
FadeTransition(navigator)
}
}
}
}
@@ -0,0 +1,9 @@
package dev.meloda.overseerr.ext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
fun <T> MutableStateFlow<T>.setValue(function: (T) -> T) {
val newValue = function(value)
update { newValue }
}
@@ -1,9 +1,43 @@
package dev.meloda.overseerr.screens.login
import dev.icerock.moko.mvvm.viewmodel.ViewModel
import androidx.lifecycle.ViewModel
import dev.meloda.overseerr.ext.setValue
import dev.meloda.overseerr.screens.login.model.LoginScreenState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class LoginViewModel : ViewModel() {
interface LoginViewModel {
val screenState: StateFlow<LoginScreenState>
fun onLoginInputChanged(newLogin: String)
fun onPasswordInputChanged(newPassword: String)
fun onAuthorizeButtonClicked()
fun onPasswordVisibilityButtonClicked()
}
class LoginViewModelImpl : ViewModel(), LoginViewModel {
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
override fun onLoginInputChanged(newLogin: String) {
screenState.setValue { old ->
old.copy(login = newLogin)
}
}
override fun onPasswordInputChanged(newPassword: String) {
screenState.setValue { old ->
old.copy(password = newPassword)
}
}
override fun onAuthorizeButtonClicked() {
// TODO: 05/08/2024, Danil Nikolaev: add logger
}
override fun onPasswordVisibilityButtonClicked() {
screenState.setValue { old ->
old.copy(isPasswordVisible = !old.isPasswordVisible)
}
}
}
@@ -1,9 +1,7 @@
package dev.meloda.overseerr.screens.login.di
import dev.icerock.moko.mvvm.compose.viewModelFactory
import dev.meloda.overseerr.screens.login.LoginViewModel
import org.koin.dsl.module
val loginModule = module {
viewModelFactory(::LoginViewModel)
}
@@ -1,4 +1,19 @@
package dev.meloda.overseerr.screens.login.model
class LoginScreenState {
data class LoginScreenState(
val login: String,
val password: String,
val isPasswordVisible: Boolean,
val isLoginEmptyError: Boolean,
val isPasswordEmptyError: Boolean
) {
companion object {
val EMPTY: LoginScreenState = LoginScreenState(
login = "",
password = "",
isPasswordVisible = false,
isLoginEmptyError = false,
isPasswordEmptyError = false
)
}
}
@@ -1,20 +1,29 @@
package dev.meloda.overseerr.screens.login.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
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.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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.meloda.overseerr.screens.login.LoginViewModel
import dev.meloda.overseerr.screens.login.LoginViewModelImpl
import dev.meloda.overseerr.screens.login.model.LoginScreenState
class LoginScreen : Screen {
@@ -22,12 +31,14 @@ class LoginScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: LoginViewModel = viewModel { LoginViewModelImpl() }
val screenState: LoginScreenState by viewModel.screenState.collectAsState()
var loginValue by rememberSaveable {
mutableStateOf("")
mutableStateOf(screenState.login)
}
var passwordValue by rememberSaveable {
mutableStateOf("")
mutableStateOf(screenState.password)
}
Scaffold(
@@ -54,13 +65,16 @@ class LoginScreen : Screen {
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = loginValue,
onValueChange = { newText -> loginValue = newText },
placeholder = { Text(text = "Login") }
onValueChange = { newText ->
loginValue = newText
viewModel.onLoginInputChanged(newText)
},
placeholder = { Text(text = "Login") },
isError = screenState.isLoginEmptyError,
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
@@ -68,8 +82,35 @@ class LoginScreen : Screen {
TextField(
modifier = Modifier.fillMaxWidth(0.9f),
value = passwordValue,
onValueChange = { newText -> passwordValue = newText },
placeholder = { Text(text = "Password") }
onValueChange = { newText ->
passwordValue = newText
viewModel.onPasswordInputChanged(newText)
},
placeholder = { Text(text = "Password") },
isError = screenState.isPasswordEmptyError,
trailingIcon = {
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
Icon(
imageVector = if (screenState.isPasswordVisible) {
Icons.Rounded.VisibilityOff
} else {
Icons.Rounded.Visibility
},
contentDescription = if (screenState.isPasswordVisible) "Password visible icon"
else "Password invisible icon"
)
}
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Password
),
visualTransformation = if (screenState.isPasswordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
@@ -80,6 +121,11 @@ class LoginScreen : Screen {
}
) {
Text(text = "Authorize")
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowForward,
contentDescription = null
)
}
}
}
@@ -1,6 +1,6 @@
package dev.meloda.overseerr.screens.url
import dev.icerock.moko.mvvm.viewmodel.ViewModel
import androidx.lifecycle.ViewModel
class UrlViewModel : ViewModel() {
}
@@ -1,11 +1,7 @@
package dev.meloda.overseerr.screens.url.di
import dev.icerock.moko.mvvm.compose.viewModelFactory
import dev.meloda.overseerr.screens.url.UrlViewModel
import org.koin.dsl.module
val urlModule = module {
viewModelFactory {
UrlViewModel()
}
}
@@ -2,23 +2,31 @@ package dev.meloda.overseerr.screens.url.presentation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
import dev.meloda.overseerr.screens.url.UrlViewModel
class UrlScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Scaffold(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
val viewModel: UrlViewModel = viewModel { UrlViewModel() }
Scaffold(modifier = Modifier.fillMaxSize()) { padding ->
Column(
modifier = Modifier.fillMaxSize()
.padding(padding)
) {
Text(text = "Input url screen")
Button(
onClick = {