Validating forms in an android project both xml and compose
Validating forms is a good practice when getting information from a user. This prevents the user from entering wrong details for example an input that requires a name but the user submits without a name. This would be a bad practice as the information entered would not be useful to where it would be used.
For the example below, we are going to create a login screen that takes in a username, email and password.
Validation in an XML project
For the project, we are going to use jetpack navigation component to navigate from our login screen to home screen. Create a new project and inside the main activity layout add this:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="55dp"
app:navGraph = "@navigation/nav_graph"
app:defaultNavHost="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="FragmentTagUsage"/>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
findNavController(R.id.nav_host_fragment)
}
}
Then create a new fragment and call it LoginFragment.
fragment_login.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".login.LoginFragment">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login_btn"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/create_acc_txt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:text="log in"
android:layout_marginTop="64dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/l_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_marginTop="23dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColorHint="@color/hintColor"
app:placeholderText="Eg.John"
app:expandedHintEnabled="false"
android:hint="Username"
app:helperTextEnabled="true"
app:helperTextTextColor="@color/errorColor"
app:hintTextColor="@color/hintColor"
app:boxStrokeColor="@color/colorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/create_acc_txt">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPersonName"
android:textColor="#525252" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/l_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_marginTop="23dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColorHint="@color/hintColor"
app:placeholderText="Eg.JohnDoe@gmail.com"
app:expandedHintEnabled="false"
android:hint="Email"
app:helperTextEnabled="true"
app:helperTextTextColor="@color/errorColor"
app:hintTextColor="@color/hintColor"
app:boxStrokeColor="@color/colorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_username">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textEmailAddress"
android:textColor="#525252" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/l_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_marginTop="15dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColorHint="@color/hintColor"
android:hint="password"
app:passwordToggleEnabled="true"
app:placeholderText=""
app:expandedHintEnabled="false"
app:helperTextEnabled="true"
app:helperTextTextColor="@color/errorColor"
app:hintTextColor="@color/hintColor"
app:boxStrokeColor="@color/colorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword"
android:textColor="#525252" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/login_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:textColor="@color/colorPrimary"
android:layout_margin="16dp"
android:background="@drawable/button_bg"
app:backgroundTint="@null"
android:backgroundTint="@null"
android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@+id/l_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Before we continue, we are going to create validations to the login form. First create a ValidateResult class and add the validations class.
ValidateResult.kt
data class ValidationResult(
val errorMessage: String = "",
val isSuccessful: Boolean = false
)
ValidateUsername.kt
class ValidateUsername {
fun execute(username: String): ValidationResult {
if (username.isBlank()) {
return ValidationResult(
isSuccessful = false,
errorMessage = "Username cannot be blank"
)
}
return ValidationResult(
isSuccessful = true
)
}
}
validateEmail.kt
class ValidateEmail {
fun execute(email: String): ValidationResult {
if(email.isBlank()) {
return ValidationResult(
isSuccessful = false,
errorMessage = "The email can't be blank"
)
}
if(!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
return ValidationResult(
isSuccessful = false,
errorMessage = "That's not a valid email"
)
}
return ValidationResult(
isSuccessful = true
)
}
}
validatePassword.kt
class ValidatePassword {
fun executePassword(password: String) : ValidationResult {
if(password.isEmpty()) {
return ValidationResult(
isSuccessful = false,
errorMessage = "Password cannot be blank"
)
}
return ValidationResult(
isSuccessful = true
)
}
}
Then create a loginviewmodel where we are going to get the state of the login form.
Create a separate class to hold initial state of the login form. Call it LoginState.kt
LoginState.kt
data class LoginState(
var username: String = "",
var usernameError: String? = null,
var email: String = "",
var emailError: String? = null,
var password: String = "",
var passwordError: String? = null
)
I will be using a mutable state flow and mutable shared flow. To read more information on using state flows visit flows.
Add this to the LoginViewModel
private val _loginState = MutableStateFlow(LoginState())
val loginState = _loginState.asStateFlow()
Create another sealed class that will hold the change of events from the input texts.
sealed class LoginFormEvent {
data class UsernameChanged(val username: String): LoginFormEvent()
data class EmailChanged(val email: String): LoginFormEvent()
data class PasswordChanged(val password: String): LoginFormEvent()
object Submit: LoginFormEvent()
}
Create a function called onEvent that will be used inside the login fragment. In the submit event, we will add its function later (submitData).
fun onEvent(event: LoginFormEvent) {
when(event) {
is LoginFormEvent.UsernameChanged -> {
_loginState.value = _loginState.value.copy(
username = event.username
)
}
is LoginFormEvent.EmailChanged -> {
_loginState.value = _loginState.value.copy(
email = event.email
)
}
is LoginFormEvent.PasswordChanged -> {
_loginState.value = _loginState.value.copy(
password = event.password
)
}
is LoginFormEvent.Submit -> {
}
}
}
Inside the LoginFragment in the onviewCreated add this
binding.apply {
viewModel.apply {
etUsername.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!) {
onEvent(LoginViewModel.LoginFormEvent.UsernameChanged(text.toString()))
loginState.value.usernameError = null
}
else {
loginState.value.username = ""
}
}
etEmail.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!){
onEvent(LoginViewModel.LoginFormEvent.EmailChanged(text.toString()))
loginState.value.emailError = null
}
else {
loginState.value.email = ""
}
}
etPassword.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!){
onEvent(LoginViewModel.LoginFormEvent.PasswordChanged(text.toString()))
loginState.value.passwordError = null
}
else{
loginState.value.password = ""
}
}
loginBtn.setOnClickListener {
onEvent(LoginViewModel.LoginFormEvent.Submit)
root.hideKeyboard()
}
}
}
Back in the LoginViewModel, we will a new sealed class that holds the login events.
private val _loginEvent = MutableSharedFlow<LoginEvent>()
val loginEvent = _loginEvent.asSharedFlow()
sealed class LoginEvent {
object Loading: LoginEvent()
data class Error(val errorMessage: String): LoginEvent()
data class Success(val message: String): LoginEvent()
}
Then add this on top, inside the viewmodel:
private val validateUsername: ValidateUsername = ValidateUsername()
private val validateEmail: ValidateEmail = ValidateEmail()
private val validatePassword: ValidatePassword = ValidatePassword()
And create submitData function that would execute the login button.
private fun submitData() {
val usernameResult = validateUsername.execute(_loginState.value.username)
val emailResult = validateEmail.execute(_loginState.value.email)
val passwordResult = validatePassword.executePassword(_loginState.value.password)
val hasError = listOf(
usernameResult,
emailResult,
passwordResult
).any { !it.isSuccessful }
if (hasError) {
_loginState.value = _loginState.value.copy(
usernameError = usernameResult.errorMessage,
emailError = emailResult.errorMessage,
passwordError = passwordResult.errorMessage
)
return
}
viewModelScope.launch {
_loginEvent.emit(LoginEvent.Loading)
delay(3000L)
_loginEvent.emit(LoginEvent.Success("Login Successful"))
}
}
In order to display the error messages and login events the states needs to be collected inside a coroutine scope.
LoginFragment.kt
CoroutineScope(Dispatchers.Main).launch {
viewModel.loginState.collectLatest { state ->
binding.apply {
lUsername.helperText = state.usernameError
lEmail.helperText = state.emailError
lPassword.helperText = state.passwordError
}
}
}
lifecycleScope.launchWhenStarted {
viewModel.loginEvent.collectLatest { event ->
when(event) {
is LoginViewModel.LoginEvent.Loading -> {
binding.progressBar.isVisible = true
}
is LoginViewModel.LoginEvent.Success -> {
binding.progressBar.isVisible = false
showToast(event.message)
findNavController().navigateSafely(R.id.action_loginFragment_to_homeFragment)
}
is LoginViewModel.LoginEvent.Error -> {}
}
}
}
Final LoginFragment.kt
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentLoginBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.apply {
viewModel.apply {
etUsername.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!) {
onEvent(LoginViewModel.LoginFormEvent.UsernameChanged(text.toString()))
loginState.value.usernameError = null
}
else {
loginState.value.username = ""
}
}
etEmail.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!){
onEvent(LoginViewModel.LoginFormEvent.EmailChanged(text.toString()))
loginState.value.emailError = null
}
else {
loginState.value.email = ""
}
}
etPassword.doOnTextChanged { text, _, _, _ ->
if (text?.isNotEmpty()!!){
onEvent(LoginViewModel.LoginFormEvent.PasswordChanged(text.toString()))
loginState.value.passwordError = null
}
else{
loginState.value.password = ""
}
}
loginBtn.setOnClickListener {
onEvent(LoginViewModel.LoginFormEvent.Submit)
root.hideKeyboard()
}
}
}
CoroutineScope(Dispatchers.Main).launch {
viewModel.loginState.collectLatest { state ->
binding.apply {
lUsername.helperText = state.usernameError
lEmail.helperText = state.emailError
lPassword.helperText = state.passwordError
}
}
}
lifecycleScope.launchWhenStarted {
viewModel.loginEvent.collectLatest { event ->
when(event) {
is LoginViewModel.LoginEvent.Loading -> {
binding.progressBar.isVisible = true
}
is LoginViewModel.LoginEvent.Success -> {
binding.progressBar.isVisible = false
showToast(event.message)
findNavController().navigateSafely(R.id.action_loginFragment_to_homeFragment)
}
is LoginViewModel.LoginEvent.Error -> {}
}
}
}
}
}
final LoginViewModel.kt
class LoginViewModel: ViewModel() {
//login event after submitting the form
private val _loginEvent = MutableSharedFlow<LoginEvent>()
val loginEvent = _loginEvent.asSharedFlow()
//state of the forms
private val _loginState = MutableStateFlow(LoginState())
val loginState = _loginState.asStateFlow()
//validate functions
private val validateUsername: ValidateUsername = ValidateUsername()
private val validateEmail: ValidateEmail = ValidateEmail()
private val validatePassword: ValidatePassword = ValidatePassword()
//event to be used inside the login fragment
fun onEvent(event: LoginFormEvent) {
when(event) {
is LoginFormEvent.UsernameChanged -> {
_loginState.value = _loginState.value.copy(
username = event.username
)
}
is LoginFormEvent.EmailChanged -> {
_loginState.value = _loginState.value.copy(
email = event.email
)
}
is LoginFormEvent.PasswordChanged -> {
_loginState.value = _loginState.value.copy(
password = event.password
)
}
is LoginFormEvent.Submit -> {
submitData()
}
}
}
private fun submitData() {
val usernameResult = validateUsername.execute(_loginState.value.username)
val emailResult = validateEmail.execute(_loginState.value.email)
val passwordResult = validatePassword.executePassword(_loginState.value.password)
val hasError = listOf(
usernameResult,
emailResult,
passwordResult
).any { !it.isSuccessful }
if (hasError) {
_loginState.value = _loginState.value.copy(
usernameError = usernameResult.errorMessage,
emailError = emailResult.errorMessage,
passwordError = passwordResult.errorMessage
)
return
}
viewModelScope.launch {
_loginEvent.emit(LoginEvent.Loading)
delay(3000L)
_loginEvent.emit(LoginEvent.Success("Login Successful"))
}
}
sealed class LoginFormEvent {
data class UsernameChanged(val username: String): LoginFormEvent()
data class EmailChanged(val email: String): LoginFormEvent()
data class PasswordChanged(val password: String): LoginFormEvent()
object Submit: LoginFormEvent()
}
sealed class LoginEvent {
object Loading: LoginEvent()
data class Error(val errorMessage: String): LoginEvent()
data class Success(val message: String): LoginEvent()
}
}
This screenshot shows when all the input fields are blank.
Follow this link for the source code.
Validation in Compose project
In compose project, we will use the same validation classes with the LoginViewModel that we created in the xml project.
Inside the MainActivity.kt, create a navigation component :
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NavGraph.LoginScreen.route
) {
composable(route = NavGraph.LoginScreen.route) {
LoginScreen(navController = navController)
}
composable(route = NavGraph.HomeScreen.route) {
HomeScreen()
}
}
}
sealed class NavGraph(val route: String) {
object LoginScreen: NavGraph("login_screen")
object HomeScreen: NavGraph("home_screen")
}
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Navigation()
}
}
}
}
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NavGraph.LoginScreen.route
) {
composable(route = NavGraph.LoginScreen.route) {
LoginScreen(navController = navController)
}
composable(route = NavGraph.HomeScreen.route) {
HomeScreen()
}
}
}
sealed class NavGraph(val route: String) {
object LoginScreen: NavGraph("login_screen")
object HomeScreen: NavGraph("home_screen")
}
Then create an input component to be used inside the login screen :
@Composable
fun Input(
placeholder: String,
label: String,
value: String,
onValueChange: (String) -> Unit,
errorMessage: String,
isError: Boolean,
keyboardType: KeyboardType = KeyboardType.Text
) {
Box(modifier = Modifier.padding(8.dp)) {
Column {
var passwordVisible by remember {
mutableStateOf(false)
}
OutlinedTextField(
modifier =Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
placeholder = {
Text(text = placeholder)
},
isError = isError,
label = {
Text(
text = label,
modifier = Modifier.padding(bottom = 8.dp)
)
},
colors = TextFieldDefaults.textFieldColors(
focusedLabelColor = Color.DarkGray,
focusedIndicatorColor = Teal,
unfocusedIndicatorColor = Color.Black,
placeholderColor = Color.Gray,
unfocusedLabelColor = Color.Black,
cursorColor = Teal,
textColor = Color.Black
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
visualTransformation = if (!passwordVisible && keyboardType == KeyboardType.Password) PasswordVisualTransformation()
else VisualTransformation.None,
trailingIcon = {
if(keyboardType == KeyboardType.Password) {
val image = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, contentDescription = null)
}
}
}
)
if (isError) {
Text(text = errorMessage, color = Color.Red, fontSize = 12.sp)
}
}
}
}
Inside the LoginScreen.kt add the login form components :
@Composable
fun LoginScreen(
navController: NavController
) {
val viewModel = LoginViewModel()
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color.White)
) {
Column {
Spacer(modifier = Modifier.height(30.dp))
Text(
text = "Login",
color = Teal200,
modifier = Modifier.padding(16.dp),
fontSize = 24.sp
)
FormSection(viewModel = viewModel)
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun FormSection(
viewModel: LoginViewModel
) {
val loginState = viewModel.loginState.value
val keyboardController = LocalSoftwareKeyboardController.current
Box(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(8.dp)) {
Input(
placeholder = "Username",
label = "Username",
value = loginState.username,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.UsernameChanged(it.trim())) },
errorMessage = loginState.usernameError,
isError = !loginState.usernameError.isEmpty()
)
Input(
placeholder = "Email",
label = "Email",
value = loginState.email,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.EmailChanged(it.trim())) },
errorMessage = loginState.emailError,
isError = !loginState.emailError.isEmpty()
)
Input(
placeholder = "Password",
label = "Password",
value = loginState.password,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.PasswordChanged(it.trim())) },
errorMessage = loginState.passwordError,
isError = !loginState.passwordError.isEmpty(),
keyboardType = KeyboardType.Password
)
Button(
onClick = {
viewModel.onEvent(LoginViewModel.LoginFormEvent.Submit)
keyboardController?.hide()
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Yellow
)
) {
Text(text = "Login", color = Teal)
}
}
}
}
Collecting states in compose can be collected inside a Launched Effect. Inside the launched effect i will display messages in a snackbar. You are free to use other components to show the loading state and messages for example, CircularProgressBar and toasts.
LaunchedEffect(key1 = true) {
viewModel.loginEvent.collectLatest { event ->
when(event) {
is LoginViewModel.LoginEvent.Loading -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "loading ..."
)
}
is LoginViewModel.LoginEvent.Success -> {
scaffoldState.snackbarHostState.showSnackbar(
message = event.message
)
navController.navigate(NavGraph.HomeScreen.route)
}
is LoginViewModel.LoginEvent.Error -> {
}
}
}
}
Final LoginScreen.kt
@Composable
fun LoginScreen(
navController: NavController
) {
val viewModel = LoginViewModel()
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = true) {
viewModel.loginEvent.collectLatest { event ->
when(event) {
is LoginViewModel.LoginEvent.Loading -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "loading ..."
)
}
is LoginViewModel.LoginEvent.Success -> {
scaffoldState.snackbarHostState.showSnackbar(
message = event.message
)
navController.navigate(NavGraph.HomeScreen.route)
}
is LoginViewModel.LoginEvent.Error -> {
}
}
}
}
Scaffold(scaffoldState = scaffoldState) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color.White)
) {
Column {
Spacer(modifier = Modifier.height(30.dp))
Text(
text = "Login",
color = Teal200,
modifier = Modifier.padding(16.dp),
fontSize = 24.sp
)
FormSection(viewModel = viewModel)
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun FormSection(
viewModel: LoginViewModel
) {
val loginState = viewModel.loginState.value
val keyboardController = LocalSoftwareKeyboardController.current
Box(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(8.dp)) {
Input(
placeholder = "Username",
label = "Username",
value = loginState.username,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.UsernameChanged(it.trim())) },
errorMessage = loginState.usernameError,
isError = !loginState.usernameError.isEmpty()
)
Input(
placeholder = "Email",
label = "Email",
value = loginState.email,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.EmailChanged(it.trim())) },
errorMessage = loginState.emailError,
isError = !loginState.emailError.isEmpty()
)
Input(
placeholder = "Password",
label = "Password",
value = loginState.password,
onValueChange = { viewModel.onEvent(LoginViewModel.LoginFormEvent.PasswordChanged(it.trim())) },
errorMessage = loginState.passwordError,
isError = !loginState.passwordError.isEmpty(),
keyboardType = KeyboardType.Password
)
Button(
onClick = {
viewModel.onEvent(LoginViewModel.LoginFormEvent.Submit)
keyboardController?.hide()
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Yellow
)
) {
Text(text = "Login", color = Teal)
}
}
}
}
The screenshot below shows message when empty input is submitted:
Follow this link for the source code.
I am Sheldon Okware a Android engineer. Follow me on Github for more.