Sitemap
Pandiyan Mani

Pandiyan Mani

Knowledge is everything.Check out my app at https://play.google.com/store/apps/details?id=com.tutorspot.com

9 min readJan 31, 2026

--

Register Page in Jetpack Compose

Lets see how to build the below ui:

Press enter or click to view image in full size
@Composable
fun CreateAccountScreen() {

Lets create a composable function named CreateAccountScreen

In Jetpack Compose, this line 👇

val scrollState = rememberScrollState()

means “create and remember the scroll position for a scrollable UI.”

var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var acceptedTerms by remember { mutableStateOf(false) }

mutableStateOf(...)

  • Creates a state holder
  • When the value changes → UI automatically recomposes
  • This is how Compose becomes reactive
  • 🔹 remember { ... }
  • Keeps the state across recompositions
  • Without remember, values reset on every UI update
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Color.Transparent
) { innerPadding -> }

modifier = Modifier.fillMaxSize()

  • Makes the scaffold fill the entire screen
  • Standard for full-screen pages

🔹 containerColor = Color.Transparent

  • Sets the background color of the screen
Column(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
listOf(
Color(0xFFFDE7F3),
Color(0xFFE3F2FD)
)
)
)
.padding(
innerPadding
)
.verticalScroll(scrollState) // ✅ enable scroll
.imePadding()
.padding(
start = 24.dp,
top = 24.dp,
end = 32.dp,
bottom = 24.dp
),

horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { }

🔹background(Brush.verticalGradient)

Applies a vertical gradient

Top → #FDE7F3 (pink)

Bottom → #E3F2FD (light blue)

⚠️ Background must come before padding to cover full screen.

🔹 padding(innerPadding)

Comes from Scaffold

Prevents content from going under: TopAppBar, BottomBar, System bars

✅ Correct placement

🔹verticalScroll(scrollState)

Makes the Column scrollable

Required when: Screen has many fields

Keyboard may push content off screen

🔹imePadding()

Adjusts layout when keyboard opens

Pushes content up so inputs remain visible

Essential for forms + TextFields

🔹 horizontalAlignment = Alignment.CenterHorizontally

Centers children horizontally

🔹 verticalArrangement = Arrangement.Center

Centers children vertically within available space

Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0xFFFDE7F3), // 0%
0.7f to Color(0xFFFDE7F3), // 70%
1.0f to Color(0xFFE3F2FD) // 100%
)
)

This means:

Pink stays dominant until 70% of the height

Blue starts after that

Text(
text = "Hey there,",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)

style = MaterialTheme.typography.bodyMedium

Uses Material 3 typography

bodyMedium is:

Normal paragraph / supporting text

Smaller than titles, larger than captions

Automatically adapts to: Font scaling (accessibility)

Text(
text = "Create an Account",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)

This is a clean, reusable custom TextField composable for auth screens.
Let’s walk through it piece by piece and then I’ll show improvements + best practices.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthTextField(
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
leadingIcon: ImageVector,
error: String? = null,
trailingIcon: @Composable (() -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None
) {
Column {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(placeholder) },
leadingIcon = { Icon(leadingIcon, null) },
trailingIcon = trailingIcon,
isError = error != null,
shape = RoundedCornerShape(12.dp),
visualTransformation = visualTransformation
)

if (error != null) {
Text(
text = error,
color = MaterialTheme.colorScheme.error,
fontSize = 12.sp,
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
)
}

Spacer(modifier = Modifier.height(12.dp))
}
}

value: String

  • Current text value
  • Comes from mutableStateOf

onValueChange: (String) -> Unit

  • Callback when user types
  • Updates state in parent

placeholder: String: Hint text shown when empty

Get Pandiyan Mani’s stories in your inbox

Join Medium for free to get updates from this writer.

leadingIcon: ImageVector->Icon shown at the start (email, lock, user, etc.)

error: String? = null

Optional error message:

null → no error

Non-null → error state + message

trailingIcon: @Composable (() -> Unit)? = null

Optional composable

Used for:

Password visibility toggle 👁

Clear button ❌

visualTransformation: VisualTransformation

Controls how text is displayed

Used for password masking

isError = error != null
Turns TextField border red

Material handles visuals automatically

shape = RoundedCornerShape(12.dp),
AuthTextField(
value = firstName,
onValueChange = { firstName = it },
placeholder = "First Name",
leadingIcon = Icons.Default.Person,
error = validationState.firstNameError
)

IconButton(onClick = { passwordVisible = !passwordVisible })

Wraps the icon to make it clickable

Toggles the boolean:

true → false

false → true

This triggers recomposition, so the icon and text update automatically.

🔹 Icon switch logic

imageVector = if (passwordVisible)
Icons.Default.Visibility
else
Icons.Default.VisibilityOff

Terms and Condition:

package com.sample.jetpackcompose

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp


@Composable
fun TermsAndConditions(
accepted: Boolean,
showError: Boolean,
onCheckedChange: (Boolean) -> Unit,
onPrivacyClick: () -> Unit,
onTermsClick: () -> Unit
) {
val annotatedText = buildAnnotatedString {
append("By continuing you accept our ")

pushStringAnnotation("PRIVACY", "privacy")
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
) {
append("Privacy Policy")
}
pop()

append(" and ")

pushStringAnnotation("TERMS", "terms")
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
) {
append("Terms of Use")
}
pop()
}

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically // ✅ center align
) {
Checkbox(
checked = accepted,
onCheckedChange = onCheckedChange,
modifier = Modifier.padding(top = 2.dp) // 🔥 fine-tune alignment
)

Spacer(modifier = Modifier.width(8.dp))

ClickableText(
text = annotatedText,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onBackground,
lineHeight = 18.sp
),
onClick = { offset ->
annotatedText
.getStringAnnotations(offset, offset)
.firstOrNull()
?.let {
when (it.tag) {
"PRIVACY" -> onPrivacyClick()
"TERMS" -> onTermsClick()
}
}
}
)
}
if (showError) {
Text(
text = "Please accept Terms & Conditions",
color = MaterialTheme.colorScheme.error,
fontSize = 12.sp,
modifier = Modifier.padding(start = 48.dp, top = 4.dp)
)
}
}
pushStringAnnotation("PRIVACY", "privacy")
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
) {
append("Privacy Policy")
}
pop()

You’re creating clickable spans inside a single Text.

🔹 pushStringAnnotation

pushStringAnnotation("PRIVACY", "privacy")

Tags a portion of text

Used later to detect which part was clicked

🔹 withStyle

withStyle(SpanStyle(...)) { append("Privacy Policy") }

Styles only that text:

Primary color

Medium weight

🔹 pop()

Ends the annotation / style scope

This pattern is exactly how Compose expects inline links to be done.

🔹 onClick = { offset -> ... }

This is the key part 🔑

What is offset?

Index of the character the user tapped

Example: clicking the “P” in “Privacy” gives its position in the string

🔹 getStringAnnotations(offset, offset)

annotatedText.getStringAnnotations(offset, offset)

Checks if the clicked character is inside an annotation

Returns a list of annotations at that position

🔹 firstOrNull()

Safely handles clicks on:

Non-clickable text

Empty areas

Prevents crashes 👍

🔹 when (it.tag)

when (it.tag) {
"PRIVACY" -> onPrivacyClick()
"TERMS" -> onTermsClick()
}

Matches the annotation tag

Triggers the correct navigation/action

Flow in simple words 🧠

User taps text

Compose tells you where they tapped (offset)

You check if that spot has an annotation

If yes → run the corresponding click handler

If no → do nothing

@Composable
fun GradientButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
contentPadding = PaddingValues()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
listOf(
Color(0xFF64B5F6),
Color(0xFF7986CB)
)
),
shape = RoundedCornerShape(28.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}

Full Working Code:

package com.sample.jetpackcompose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
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.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sample.jetpackcompose.ui.theme.JetpackComposeTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
JetpackComposeTheme {
CreateAccountScreen()
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateAccountScreen() {
val scrollState = rememberScrollState()
var validationState by remember { mutableStateOf(ValidationState()) }
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var acceptedTerms by remember { mutableStateOf(false) }

Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Color.Transparent
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0xFFFDE7F3), // start
0.7f to Color(0xFFFDE7F3), // 70%
1.0f to Color(0xFFE3F2FD) // 100%
)
)
)
.padding(
innerPadding
)
.verticalScroll(scrollState) // ✅ enable scroll
.imePadding()
.padding(
start = 24.dp,
top = 24.dp,
end = 32.dp,
bottom = 24.dp
),

horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {

Text(
text = "Hey there,",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)

Text(
text = "Create an Account",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)

Spacer(modifier = Modifier.height(32.dp))

AuthTextField(
value = firstName,
onValueChange = { firstName = it },
placeholder = "First Name",
leadingIcon = Icons.Default.Person,
error = validationState.firstNameError
)

AuthTextField(
value = lastName,
onValueChange = { lastName = it },
placeholder = "Last Name",
leadingIcon = Icons.Default.Person,
error = validationState.lastNameError
)

AuthTextField(
value = email,
onValueChange = { email = it },
placeholder = "Email",
leadingIcon = Icons.Default.Email,
error = validationState.emailError
)

AuthTextField(
value = email,
onValueChange = { email = it },
placeholder = "Email",
leadingIcon = Icons.Default.Email,
error = validationState.emailError
)

AuthTextField(
value = password,
onValueChange = { password = it },
placeholder = "Password",
leadingIcon = Icons.Default.Lock,
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible)
Icons.Default.Visibility
else Icons.Default.VisibilityOff,
contentDescription = null
)
}
},
visualTransformation = if (passwordVisible)
VisualTransformation.None
else PasswordVisualTransformation(),
error = validationState.passwordError
)

Spacer(modifier = Modifier.height(12.dp))

TermsAndConditions(
accepted = acceptedTerms,
showError = validationState.termsError != null,
onCheckedChange = { acceptedTerms = it },
onPrivacyClick = { /* open privacy */ },
onTermsClick = { /* open terms */ }
)


Spacer(modifier = Modifier.height(24.dp))

GradientButton(
text = "Register",
onClick = {
validationState = validateForm(
firstName,
lastName,
email,
password,
acceptedTerms
)

if (validationState.isValid) {
// Proceed with API call
}
}
)

Spacer(modifier = Modifier.height(24.dp))

Row(verticalAlignment = Alignment.CenterVertically) {
Divider(modifier = Modifier.weight(1f))
Text(" or ", color = Color.Gray)
Divider(modifier = Modifier.weight(1f))
}

Spacer(modifier = Modifier.height(24.dp))

Row {
Text("Already have an account? ")
Text(
text = "Login",
color = Color(0xFF5E92F3),
fontWeight = FontWeight.Bold
)
}
}
}

}


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthTextField(
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
leadingIcon: ImageVector,
error: String? = null,
trailingIcon: @Composable (() -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None
) {
Column {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(placeholder) },
leadingIcon = { Icon(leadingIcon, null) },
trailingIcon = trailingIcon,
isError = error != null,
shape = RoundedCornerShape(12.dp),
visualTransformation = visualTransformation
)

if (error != null) {
Text(
text = error,
color = MaterialTheme.colorScheme.error,
fontSize = 12.sp,
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
)
}

Spacer(modifier = Modifier.height(12.dp))
}
}


@Composable
fun GradientButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
contentPadding = PaddingValues()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
listOf(
Color(0xFF64B5F6),
Color(0xFF7986CB)
)
),
shape = RoundedCornerShape(28.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}

}
package com.sample.jetpackcompose

fun validateForm(
firstName: String,
lastName: String,
email: String,
password: String,
acceptedTerms: Boolean
): ValidationState {

return ValidationState(
firstNameError = if (firstName.isBlank()) "First name required" else null,
lastNameError = if (lastName.isBlank()) "Last name required" else null,
emailError = if (
email.isBlank() ||
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
) "Invalid email address" else null,
passwordError = if (password.length < 8)
"Password must be at least 8 characters"
else null,
termsError = if (!acceptedTerms) "Accept terms to continue" else null
)
}
package com.sample.jetpackcompose

data class ValidationState(
val firstNameError: String? = null,
val lastNameError: String? = null,
val emailError: String? = null,
val passwordError: String? = null,
val termsError: String? = null
) {
val isValid: Boolean
get() = listOf(
firstNameError,
lastNameError,
emailError,
passwordError,
termsError
).all { it == null }
}
package com.sample.jetpackcompose

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp


@Composable
fun TermsAndConditions(
accepted: Boolean,
showError: Boolean,
onCheckedChange: (Boolean) -> Unit,
onPrivacyClick: () -> Unit,
onTermsClick: () -> Unit
) {
val annotatedText = buildAnnotatedString {
append("By continuing you accept our ")

pushStringAnnotation("PRIVACY", "privacy")
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
) {
append("Privacy Policy")
}
pop()

append(" and ")

pushStringAnnotation("TERMS", "terms")
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
) {
append("Terms of Use")
}
pop()
}

Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically // ✅ center align
) {
Checkbox(
checked = accepted,
onCheckedChange = onCheckedChange,
modifier = Modifier.padding(top = 2.dp) // 🔥 fine-tune alignment
)

Spacer(modifier = Modifier.width(8.dp))

ClickableText(
text = annotatedText,
style = MaterialTheme.typography.bodySmall,
onClick = { offset ->
annotatedText
.getStringAnnotations(offset, offset)
.firstOrNull()
?.let {
when (it.tag) {
"PRIVACY" -> onPrivacyClick()
"TERMS" -> onTermsClick()
}
}
}
)
}
if (showError) {
Text(
text = "Please accept Terms & Conditions",
color = MaterialTheme.colorScheme.error,
fontSize = 12.sp,
modifier = Modifier.padding(start = 48.dp, top = 4.dp)
)
}
}

Happy Coding !!!

--

--