In this article, we will walk you through the process of integrating form validation in an Android application using Jetpack Compose. You’ll learn how to implement effective validation for fields such as name, email, password, and confirm password, enhancing the robustness of your app. By the end of this tutorial, you’ll have a solid understanding of how to validate user inputs and provide immediate feedback, making your application more user-friendly and reliable.
Step 1: Create a
UiText
Interface for Dynamic Strings and Easy Localization
The first step in building a robust form validation system is to handle strings dynamically. This is especially useful for localization and accessing string resources. We’ll create an interface called UiText
that allows us to handle both dynamic strings and string resources.
sealed interface UiText {
data class DynamicString(val value: String) : UiText
class StringResource(
@StringRes val resId: Int,
vararg val args: Any
) : UiText@Composable
fun asString(): String {
return when (this) {
is DynamicString -> value
is StringResource -> stringResource(resId, *args)
}
}
fun asString(context: Context): String {
return when (this) {
is DynamicString -> value
is StringResource -> context.getString(resId, *args)
}
}
}
Explanation:
- DynamicString: This is used for non-resource-based strings that can be created at runtime.
- StringResource: This handles strings from Android’s
strings.xml
resources and allows passing arguments to format the string. - The
asString()
function is available in two forms: one for Compose and one for traditional AndroidContext
, making it versatile for different use cases.
Step 2: Create the
IconResource
Class for Dynamic Icon Management
Next, we will handle dynamic icons in a similar way. Jetpack Compose allows us to use both drawable resources and vector images. To manage these efficiently, we’ll create an IconResource
class that supports both resource-based and vector-based icons.
class IconResource private constructor(
@DrawableRes private val resID: Int?,
private val imageVector: ImageVector?
) {@Composable
fun asPainterResource(): Painter {
resID?.let {
return painterResource(id = resID)
}
return rememberVectorPainter(image = imageVector!!)
}
companion object {
fun fromDrawableResource(@DrawableRes resID: Int): IconResource {
return IconResource(resID, null)
}
fun fromImageVector(imageVector: ImageVector): IconResource {
return IconResource(null, imageVector)
}
}
}
Explanation:
- The
IconResource
class is used to handle both drawable resource-based icons andImageVector
icons in Compose. - The
asPainterResource()
function checks whether the icon is a drawable resource or anImageVector
, then returns the appropriatePainter
to be used in Compose. - Factory Methods: The companion object provides factory methods:
fromDrawableResource()
for drawable resources andfromImageVector()
for vector images, making the API easy to use.
Step 3: Define the
FieldInput
andErrorStatus
Data Classes
To implement form validation, we need to track both the field input and its validation status. We define two simple data classes: FieldInput
for capturing the field value and whether the user has interacted with it, and ErrorStatus
for capturing any validation errors.
data class FieldInput(
val value: String = "",
val hasInteracted: Boolean = false,
)data class ErrorStatus(
val isError: Boolean,
val errorMsg: UiText? = null,
)
Explanation:
value
: Holds the current value of the input field.hasInteracted
: Tracks whether the user has interacted with the field, helping to show validation errors only after interaction.isError
: A boolean indicating if there’s an error in the field.errorMsg
: AUiText
object that holds the error message, making it easier to display error messages dynamically or using resource strings.
Step 4: Create the
OutlineFieldWithState
Composable for Input Handling
Now, we need a composable function that will render an outlined text field with proper handling for errors, icons, and visibility of password fields. The OutlineFieldWithState
composable helps to manage all of these aspects within a single component.
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
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.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.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation@Composable
fun OutlineFieldWithState(
modifier: Modifier = Modifier,
label: String,
fieldInput: FieldInput,
errorStatus: ErrorStatus,
keyboardOptions: KeyboardOptions,
isPasswordField: Boolean = false,
keyboardActions: KeyboardActions = KeyboardActions.Default,
leadingIconResource: IconResource? = null,
onValueChange: (String) -> Unit,
) {
var passwordVisible by remember { mutableStateOf(false) }
OutlinedTextField(
modifier = modifier,
value = fieldInput.value,
onValueChange = {
onValueChange(it)
},
label = {
Text(text = label, style = MaterialTheme.typography.bodyMedium)
},
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
leadingIcon = leadingIconResource?.let {
{
Icon(it.asPainterResource(), contentDescription = null)
}
},
isError = fieldInput.hasInteracted && errorStatus.isError,
supportingText = {
if (fieldInput.hasInteracted && errorStatus.isError) {
errorStatus.errorMsg?.let {
Text(
text = it.asString(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall
)
}
}
},
trailingIcon = if (isPasswordField) {
{
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
painter = if (passwordVisible) painterResource(R.drawable.ic_visibility)
else painterResource(R.drawable.ic_visibility_off),
contentDescription = if (passwordVisible) "Hide password" else "Show password"
)
}
}
} else if (fieldInput.hasInteracted && errorStatus.isError) {
{
Icon(imageVector = Icons.Filled.Info, contentDescription = null)
}
} else {
null
},
visualTransformation = if (isPasswordField && !passwordVisible) PasswordVisualTransformation() else VisualTransformation.None
)
}
Explanation:
- Password Visibility: The composable includes functionality to toggle the visibility of the password field using an
IconButton
. - Leading and Trailing Icons: You can pass an
IconResource
to add a leading icon. For password fields, a visibility toggle icon is displayed on the trailing side. - Error Handling: If the field has been interacted with and contains an error, the error message is displayed below the field. An error icon also appears on the trailing side of the input when appropriate.
- Keyboard Options: The function supports custom
keyboardOptions
andkeyboardActions
, allowing flexibility in input behavior.
Step 5: Add Dependencies for ViewModel and Create
InputViewModel
To manage the state of our form and handle input validation, we will create a ViewModel. First, ensure you have the necessary dependency in your build.gradle.kts
file:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
Next, we’ll create the InputViewModel
class that will encapsulate the logic for managing input fields and their validation statuses.
class InputViewModel : ViewModel() {
// Add properties and methods to manage form inputs and validation logic
}
Explanation:
- ViewModel: The
InputViewModel
class will extendViewModel
, allowing it to survive configuration changes such as screen rotations. - State Management: This ViewModel will hold properties for managing the state of your form inputs, including values and error statuses, which will be observed by your composables.
In the InputScreen
composable, we will initialize the InputViewModel
and structure the UI using a LazyColumn
inside a Scaffold
for form input layout.
@Composable
fun InputScreen() {
val context = LocalContext.current
val inputViewModel: InputViewModel = viewModel()Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(horizontal = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically)
) {
// UI content here, using inputViewModel state
}
}
}
Explanation:
- ViewModel Initialization: The
InputViewModel
is initialized usingviewModel()
to manage the form’s state. - LocalContext: The
LocalContext.current
is retrieved to handle context-related operations, like showing toast messages or accessing resources. - Scaffold: A
Scaffold
is used as the base layout, which allows us to manage UI components like top bars, bottom bars, and the main content area. - LazyColumn: Inside the
Scaffold
, aLazyColumn
is used to arrange form elements vertically. Padding and spacing are applied for better UI alignment.
Step 6: Create a Utils File and Add
showToast
Function and Add String Resources for Validation Messages
To enhance user experience, we can create a utility function for displaying toast messages throughout our application. This function can be used to show feedback to users based on their actions, such as input validation errors.
Create a new Kotlin file, for example, utils.kt
, and add the following code:
import android.content.Context
import android.widget.Toastfun Context.showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
Explanation:
- Context Extension Function: The
showToast
function is an extension function onContext
, allowing you to call it from any context (such as an Activity or Composable) easily. - Toast Display: It uses the
Toast.makeText
method to display a toast message with a duration ofToast.LENGTH_LONG
, providing feedback to users.
after that, we’ll add the string resources required for validation in the res/values/strings.xml
file. These string resources will be used to display validation messages throughout the app.
<resources>
<!-- App Name -->
<string name="app_name">Input Validation JC</string><!-- General Validation Messages -->
<string name="required">Required</string>
<!-- Email Validation -->
<string name="enter_the_email">Enter the Email</string>
<string name="valid_e_mail">Valid E-mail</string>
<!-- Password Validation -->
<string name="password_must_be_8_to_10_character">Password must be 8 to 10 Characters!</string>
<string name="password_don_t_match">Password Don\'t Match!</string>
<string name="enter_the_password">Enter the Password</string>
<string name="enter_the_confirm_password">Enter the Confirm Password</string>
<!-- Name Validation -->
<string name="enter_the_name">Enter the Name</string>
<!-- Submit Button -->
<string name="submit">Submit</string>
</resources>
Explanation:
- Required Field Message: The string resource for required fields is
"Required"
. - Email Validation: Strings are provided for requesting an email input and indicating invalid email formats.
- Password Validation: Strings handle password length validation and password matching issues (such as confirmation mismatches).
- Submit Button: The submit button label is stored as a string resource for consistency and localization.
Step 7: Implement Simple Name Validation
In this step, we will add a simple name validation function to the utils
file and demonstrate how to use it in the InputViewModel
and UI.
1. Add Name Validation in
utils.kt
File
The validateName
function checks if the name input is empty. If it is, it returns an ErrorStatus
with a required field error message.
fun validateName(name: String): ErrorStatus {
return when {
name.trim().isEmpty() -> {
ErrorStatus(true, UiText.StringResource(R.string.required))
}
else -> {
ErrorStatus(false)
}
}
}
2. Create ViewModel for Managing Name Input
In the InputViewModel
, we add the state for the nameField
and use the validateName
function to derive the error status based on the current input.
class InputViewModel : ViewModel() {// Name field
var nameField by mutableStateOf(FieldInput())
val nameErrorStatus by derivedStateOf {
validateName(nameField.value)
}
}
3. Add
OutlineFieldWithState
to Input Screen
Next, we include an OutlineFieldWithState
composable in the screen, which binds the input field to the ViewModel’s nameField
state and displays any validation errors.
// Name UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_name),
fieldInput = inputViewModel.nameField,
errorStatus = inputViewModel.nameErrorStatus,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
) {
inputViewModel.nameField = inputViewModel.nameField.copy(
value = it,
hasInteracted = true
)
}
}
4. Add Button for Submission
Finally, we include a Button
that checks for errors on submission. If there’s an error, it triggers the toast message; otherwise, it shows a success message.
item {
Button(
onClick = {
// Name validation
if (inputViewModel.nameErrorStatus.isError) {
inputViewModel.nameField = inputViewModel.nameField.copy(hasInteracted = true)
inputViewModel.nameErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}// Success message
context.showToast("Success")
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.submit))
}
}
Explanation:
- Name Validation: The
validateName
function ensures the name is not empty. If it is, an error message is displayed. - ViewModel: The
InputViewModel
manages the form’s state and validation logic. - Toast Messages: When an error occurs, the user receives a toast message with the error text. If the input is valid, a success message is shown.
Step 8: Example of Email Validation
In this step, we’ll add an email validation function in the utils file and update the InputViewModel
and UI components accordingly.
1. Email Validation Function
Add the following email validation logic to the utils file:
fun validateEmail(email: String): ErrorStatus {
val emailPattern = Regex("[a-zA-Z\\d._-]+@[a-z]+\\.+[a-z]+")
return when {
email.trim().isEmpty() -> {
ErrorStatus(true, UiText.StringResource(R.string.required))
}!email.trim().matches(emailPattern) -> {
ErrorStatus(true, UiText.StringResource(R.string.valid_e_mail))
}
else -> {
ErrorStatus(false)
}
}
}
Explanation:
- The function checks if the email is empty or does not match a valid email pattern and returns an appropriate
ErrorStatus
.
2. Update the
InputViewModel
Extend the InputViewModel
to handle the email field validation:
class InputViewModel : ViewModel() {
// Name field
var nameField by mutableStateOf(FieldInput())
val nameErrorStatus by derivedStateOf {
validateName(nameField.value)
}// Email field
var emailField by mutableStateOf(FieldInput())
val emailErrorStatus by derivedStateOf {
validateEmail(emailField.value)
}
}
Explanation: The emailField
is added to the ViewModel, along with emailErrorStatus
, which derives its value from the validateEmail
function.
3. Add Email Input to the UI
Update the UI to include an email input field and validation:
// Name UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_name),
fieldInput = inputViewModel.nameField,
errorStatus = inputViewModel.nameErrorStatus,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
) {
inputViewModel.nameField = inputViewModel.nameField.copy(
value = it,
hasInteracted = true
)
}
}// Email UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_email),
fieldInput = inputViewModel.emailField,
errorStatus = inputViewModel.emailErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Email),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
) {
inputViewModel.emailField = inputViewModel.emailField.copy(
value = it,
hasInteracted = true
)
}
}
Explanation: Two input fields are now present — one for the name and one for the email. The email field uses a custom validation to check the email format.
4. Button Validation Logic
Add logic for the submit button to validate both fields:
item {
Button(
onClick = {
// Name validation
if (inputViewModel.nameErrorStatus.isError) {
inputViewModel.nameField = inputViewModel.nameField.copy(hasInteracted = true)
inputViewModel.nameErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}// Email validation
if (inputViewModel.emailErrorStatus.isError) {
inputViewModel.emailField = inputViewModel.emailField.copy(hasInteracted = true)
inputViewModel.emailErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Success message
context.showToast("Success")
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.submit))
}
}
Explanation: This ensures that both the name and email fields are validated before displaying a success message.
Step 9: Example of Password Validation
In this step, we will add password validation in the utils file and update the InputViewModel
and UI components accordingly.
1. Password Validation Function
Add the following password validation logic to the utils file:
fun validatePassword(password: String): ErrorStatus {
return when {
password.trim().isEmpty() -> {
ErrorStatus(true, UiText.StringResource(R.string.required))
}
password.trim().length < 8 || password.trim().length > 10 -> {
ErrorStatus(true, UiText.StringResource(R.string.password_must_be_8_to_10_character))
}
else -> {
ErrorStatus(false)
}
}
}
Explanation: This function checks if the password is empty, or if it doesn’t meet the required length (8 to 10 characters), returning the appropriate ErrorStatus
.
2. Update the
InputViewModel
Extend the InputViewModel
to handle the password field validation:
class InputViewModel : ViewModel() {
// Name field
var nameField by mutableStateOf(FieldInput())
val nameErrorStatus by derivedStateOf {
validateName(nameField.value)
}// Email field
var emailField by mutableStateOf(FieldInput())
val emailErrorStatus by derivedStateOf {
validateEmail(emailField.value)
}
// Password field
var passwordField by mutableStateOf(FieldInput())
val passwordErrorStatus by derivedStateOf {
validatePassword(passwordField.value)
}
}
Explanation: The passwordField
is added to the ViewModel, along with passwordErrorStatus
, which derives its value from the validatePassword
function.
3. Add Password Input to the UI
Update the UI to include a password input field and validation:
// Name UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_name),
fieldInput = inputViewModel.nameField,
errorStatus = inputViewModel.nameErrorStatus,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
) {
inputViewModel.nameField = inputViewModel.nameField.copy(
value = it,
hasInteracted = true
)
}
}// Email UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_email),
fieldInput = inputViewModel.emailField,
errorStatus = inputViewModel.emailErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Email),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
) {
inputViewModel.emailField = inputViewModel.emailField.copy(
value = it,
hasInteracted = true
)
}
}
// Password UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_password),
fieldInput = inputViewModel.passwordField,
errorStatus = inputViewModel.passwordErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Lock),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
isPasswordField = true
) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(
value = it,
hasInteracted = true
)
}
}
Explanation: A new input field is added for the password, which uses the custom password validation logic.
4. Button Validation Logic
Add the validation for the password field when the submit button is clicked:
item {
Button(
onClick = {
// Name validation
if (inputViewModel.nameErrorStatus.isError) {
inputViewModel.nameField = inputViewModel.nameField.copy(hasInteracted = true)
inputViewModel.nameErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}// Email validation
if (inputViewModel.emailErrorStatus.isError) {
inputViewModel.emailField = inputViewModel.emailField.copy(hasInteracted = true)
inputViewModel.emailErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Password validation
if (inputViewModel.passwordErrorStatus.isError) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(hasInteracted = true)
inputViewModel.passwordErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Success message
context.showToast("Success")
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.submit))
}
}
Explanation: The submit button now checks for validation errors in the name, email, and password fields before proceeding. If any errors are found, appropriate error messages are displayed.
Step 10: Example of Confirm Password Validation
In this step, we will add Confirm password validation in the utils file and update the InputViewModel
and UI components accordingly.
1. Confirm Password Validation Function
Add the following Confirm password validation logic to the utils file:
fun validateConPassword(password : String,conPassword : String) : ErrorStatus{
return when {
conPassword.trim().isEmpty() -> {
ErrorStatus(true, UiText.StringResource(R.string.required))
}
conPassword.trim().length < 8 || conPassword.trim().length > 10 -> {
ErrorStatus(true, UiText.StringResource(R.string.password_must_be_8_to_10_character))
}
password.trim() != conPassword.trim() -> {
ErrorStatus(true, UiText.StringResource(R.string.password_don_t_match))
}
else -> {
ErrorStatus(false)
}
}
}
Explanation: This function checks if the confirm password field is empty, verifies if the length is between 8 and 10 characters, and ensures the password and confirm password match, returning an appropriate ErrorStatus
.
2. Update the
InputViewModel
Extend your InputViewModel
to handle the validation for the confirm password field:
class InputViewModel : ViewModel() {
// Name field
var nameField by mutableStateOf(FieldInput())
val nameErrorStatus by derivedStateOf {
validateName(nameField.value)
}// Email field
var emailField by mutableStateOf(FieldInput())
val emailErrorStatus by derivedStateOf {
validateEmail(emailField.value)
}
// Password field
var passwordField by mutableStateOf(FieldInput())
val passwordErrorStatus by derivedStateOf {
validatePassword(passwordField.value)
}
// Confirm Password field
var conPasswordField by mutableStateOf(FieldInput())
val conPasswordErrorStatus by derivedStateOf {
validateConPassword(passwordField.value, conPasswordField.value)
}
}
Explanation: The conPasswordField
is added to the ViewModel along with conPasswordErrorStatus
, which uses the validateConPassword
function to derive its value.
3. Add Confirm Password Input to the UI
Update the UI to include a confirm password input field and validation:
// Name UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_name),
fieldInput = inputViewModel.nameField,
errorStatus = inputViewModel.nameErrorStatus,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
) {
inputViewModel.nameField = inputViewModel.nameField.copy(
value = it,
hasInteracted = true
)
}
}// Email UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_email),
fieldInput = inputViewModel.emailField,
errorStatus = inputViewModel.emailErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Email),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
) {
inputViewModel.emailField = inputViewModel.emailField.copy(
value = it,
hasInteracted = true
)
}
}
// Password UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_password),
fieldInput = inputViewModel.passwordField,
errorStatus = inputViewModel.passwordErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Lock),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
isPasswordField = true
) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(
value = it,
hasInteracted = true
)
}
}
// Confirm Password UI
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_confirm_password),
fieldInput = inputViewModel.conPasswordField,
errorStatus = inputViewModel.conPasswordErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Lock),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
isPasswordField = true
) {
inputViewModel.conPasswordField = inputViewModel.conPasswordField.copy(
value = it,
hasInteracted = true
)
}
}
Explanation: A new input field is added for the confirm password, which uses the custom confirm password validation logic.
4. Button Validation Logic
Add the validation for the confirm password field when the submit button is clicked:
item {
Button(
onClick = {
// Name validation
if (inputViewModel.nameErrorStatus.isError) {
inputViewModel.nameField = inputViewModel.nameField.copy(hasInteracted = true)
inputViewModel.nameErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}// Email validation
if (inputViewModel.emailErrorStatus.isError) {
inputViewModel.emailField = inputViewModel.emailField.copy(hasInteracted = true)
inputViewModel.emailErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Password validation
if (inputViewModel.passwordErrorStatus.isError) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(hasInteracted = true)
inputViewModel.passwordErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Confirm Password validation
if (inputViewModel.conPasswordErrorStatus.isError) {
inputViewModel.conPasswordField = inputViewModel.conPasswordField.copy(hasInteracted = true)
inputViewModel.conPasswordErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
// Success message
context.showToast("Success")
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.submit))
}
}
Explanation: The submit button now validates all fields, including the confirm password, and displays relevant error messages if validation fails. If all validations pass, a success message is shown.
Here’s your complete InputScreen
implementation along with the MainActivity
class. I’ve ensured that all the fields, including the confirm password field and the validation logic for each input, are included:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
InputValidationJCTheme {
InputScreen()
}
}
}
}@Composable
fun InputScreen() {
val context = LocalContext.current
val inputViewModel: InputViewModel = viewModel()
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(horizontal = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically)
) {
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_name),
fieldInput = inputViewModel.nameField,
errorStatus = inputViewModel.nameErrorStatus,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
) {
inputViewModel.nameField = inputViewModel.nameField.copy(
value = it,
hasInteracted = true
)
}
}
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_email),
fieldInput = inputViewModel.emailField,
errorStatus = inputViewModel.emailErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Email),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
) {
inputViewModel.emailField = inputViewModel.emailField.copy(
value = it,
hasInteracted = true
)
}
}
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_password),
fieldInput = inputViewModel.passwordField,
errorStatus = inputViewModel.passwordErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Lock),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
isPasswordField = true
) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(
value = it,
hasInteracted = true
)
}
}
item {
OutlineFieldWithState(
modifier = Modifier.fillMaxWidth(),
label = stringResource(R.string.enter_the_confirm_password),
fieldInput = inputViewModel.conPasswordField,
errorStatus = inputViewModel.conPasswordErrorStatus,
leadingIconResource = IconResource.fromImageVector(Icons.Filled.Lock),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
isPasswordField = true
) {
inputViewModel.conPasswordField = inputViewModel.conPasswordField.copy(
value = it,
hasInteracted = true
)
}
}
item {
Button(
onClick = {
if (inputViewModel.nameErrorStatus.isError) {
inputViewModel.nameField = inputViewModel.nameField.copy(hasInteracted = true)
inputViewModel.nameErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
if (inputViewModel.emailErrorStatus.isError) {
inputViewModel.emailField = inputViewModel.emailField.copy(hasInteracted = true)
inputViewModel.emailErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
if (inputViewModel.passwordErrorStatus.isError) {
inputViewModel.passwordField = inputViewModel.passwordField.copy(hasInteracted = true)
inputViewModel.passwordErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
if (inputViewModel.conPasswordErrorStatus.isError) {
inputViewModel.conPasswordField = inputViewModel.conPasswordField.copy(hasInteracted = true)
inputViewModel.conPasswordErrorStatus.errorMsg?.let {
context.showToast(it.asString(context))
}
return@Button
}
context.showToast("Success")
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.submit))
}
}
}
}
}
- The
InputScreen
function displays all the input fields, including name, email, password, and confirm password. - Each input field utilizes the
OutlineFieldWithState
composable, which is presumably a custom composable for handling input fields with state management. - The submit button validates each field before proceeding, showing appropriate error messages using a toast.
- Ensure that the utility functions (
validateName
,validateEmail
,validatePassword
, andvalidateConPassword
) and theInputViewModel
are defined correctly to provide the necessary validation logic.
You’ve successfully integrated form validation with Jetpack Compose in Android Studio. This implementation enhances your app’s reliability by ensuring that user inputs are correctly validated, providing a smoother user experience. Now, you can run the app to ensure everything works smoothly and see the benefits of your validation logic in action.
Happy coding!
Example GitHub Repo:
If you’re interested in learning more about Kotlin Multiplatform and Compose Multiplatform, check out my playlist on YouTube Channel:
Mastering Kotlin Multiplatform with Jetpack Compose: Complete Guide in Hindi