Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update documentation for state based TF #487

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicSecureTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.TextObfuscationMode
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
Expand Down Expand Up @@ -328,6 +330,27 @@ private object TextBrushSnippet2 {
}
}

private object TextBrushSnippet2StateBased {
@Composable
fun TextStyledBrushSnippet() {
val rainbowColors: List<Color> = listOf()
// [START android_compose_text_textfield_state_brush]
var text by remember { mutableStateOf("") }
val brush = remember {
Brush.linearGradient(
colors = rainbowColors
)
}

TextField(
state = rememberTextFieldState(),
placeholder = { Text("username") },
textStyle = TextStyle(brush = brush)
)
// [END android_compose_text_textfield_state_brush]
}
}

private object TextBrushSnippet3 {
@Composable
fun TextStyledBrushSnippet() {
Expand Down Expand Up @@ -449,6 +472,19 @@ private object TextTextFieldSnippet {
// [END android_compose_text_textfield_filled]
}

private object TextTextFieldStateSnippet {
// [START android_compose_text_textfield_state_filled]
@Composable
fun SimpleFilledTextFieldSample() {

TextField(
state = TextFieldState(),
label = { Text("Label") }
)
}
// [END android_compose_text_textfield_state_filled]
}

private object TextOutlinedTextFieldSnippet {
// [START android_compose_text_textfield_outlined]
@Composable
Expand All @@ -464,6 +500,18 @@ private object TextOutlinedTextFieldSnippet {
// [END android_compose_text_textfield_outlined]
}

private object TextOutlinedTextFieldStateSnippet {
// [START android_compose_text_textfield_state_outlined]
@Composable
fun SimpleOutlinedTextFieldSample() {
OutlinedTextField(
state = rememberTextFieldState(),
label = { Text("Label") }
)
}
// [END android_compose_text_textfield_state_outlined]
}

private object TextStylingTextFieldSnippet {
// [START android_compose_text_textfield_styled]
@Composable
Expand All @@ -482,6 +530,23 @@ private object TextStylingTextFieldSnippet {
// [END android_compose_text_textfield_styled]
}

private object TextStylingTextFieldStateSnippet {
// [START android_compose_text_textfield_state_styled]
@Composable
fun StyledTextField() {
var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }

TextField(
state = rememberTextFieldState(),
label = { Text("Enter text") },
lineLimits = TextFieldLineLimits.MultiLine(2),
textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
)
}
// [END android_compose_text_textfield_state_styled]
}

private object TextFormattingTextFieldSnippet {
// [START android_compose_text_textfield_visualtransformation]
@Composable
Expand All @@ -499,6 +564,25 @@ private object TextFormattingTextFieldSnippet {
// [END android_compose_text_textfield_visualtransformation]
}

private object TextFormattingTextFieldStateSnippet {
// [START android_compose_text_textfield_visualtransformation]

// TODO: update use a different transform
@Composable
fun PasswordTextField() {
var password by rememberSaveable { mutableStateOf("") }

TextField(
value = password,
onValueChange = { password = it },
label = { Text("Enter password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
}
// [END android_compose_text_textfield_visualtransformation]
}

private object TextCleanInputSnippet {
// [START android_compose_text_textfield_clean_input]
@Composable
Expand All @@ -514,6 +598,22 @@ private object TextCleanInputSnippet {
// [END android_compose_text_textfield_clean_input]
}

private object TextStateCleanInputSnippet {
// [START android_compose_text_textfield_clean_input]
@Composable
fun NoLeadingZeroes() {
// TODO: update this - use inputTransformation instead?
var input by rememberSaveable { mutableStateOf("") }
TextField(
value = input,
onValueChange = { newText ->
input = newText.trimStart { it == '0' }
}
)
}
// [END android_compose_text_textfield_clean_input]
}

/** Effective State management **/

private object TextEffectiveStateManagement1 {
Expand Down Expand Up @@ -560,6 +660,39 @@ private object TextEffectiveStateManagement2 {
// [END android_compose_text_state_management]
}

private object TextStateEffectiveStateManagement2 {
// TODO: update
class UserRepository

private val viewModel = SignUpViewModel(UserRepository())

// [START android_compose_text_state_management]
// SignUpViewModel.kt

class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() {

var username by mutableStateOf("")
private set

fun updateUsername(input: String) {
username = input
}
}

// SignUpScreen.kt

@Composable
fun SignUpScreen(/*...*/) {

OutlinedTextField(
value = viewModel.username,
onValueChange = { username -> viewModel.updateUsername(username) }
/*...*/
)
}
// [END android_compose_text_state_management]
}

// [START android_compose_text_link_1]
@Composable
fun AnnotatedStringWithLinkSample() {
Expand Down Expand Up @@ -816,6 +949,30 @@ fun PhoneNumber() {
}
// [END android_compose_text_auto_format_phone_number_textfieldconfig]

// [START android_compose_text_auto_format_phone_number_textfieldconfig]
@Composable
fun PhoneNumberStateBased() {
// TODO: update to use output transformation
var phoneNumber by rememberSaveable { mutableStateOf("") }
val numericRegex = Regex("[^0-9]")
TextField(
value = phoneNumber,
onValueChange = {
// Remove non-numeric characters.
val stripped = numericRegex.replace(it, "")
phoneNumber = if (stripped.length >= 10) {
stripped.substring(0..9)
} else {
stripped
}
},
label = { Text("Enter Phone Number") },
visualTransformation = NanpVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
// [END android_compose_text_auto_format_phone_number_textfieldconfig]

// [START android_compose_text_auto_format_phone_number_transformtext]
class NanpVisualTransformation : VisualTransformation {

Expand Down Expand Up @@ -909,6 +1066,52 @@ fun PasswordTextField() {
}
// [END android_compose_text_showhidepassword]

// [START android_compose_text_showhidepassword]
@Composable
fun SecureTextField() {
// TODO: update to SecureTextField
val state = remember { TextFieldState() }
var showPassword by remember { mutableStateOf(false) }
BasicSecureTextField(
state = state,
textObfuscationMode =
if (showPassword) {
TextObfuscationMode.Visible
} else {
TextObfuscationMode.RevealLastTyped
},
modifier = Modifier
.fillMaxWidth()
.padding(6.dp)
.border(1.dp, Color.LightGray, RoundedCornerShape(6.dp))
.padding(6.dp),
decorator = { innerTextField ->
Box(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp, end = 48.dp)
) {
innerTextField()
}
Icon(
if (showPassword) {
Icons.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
},
contentDescription = "Toggle password visibility",
modifier = Modifier
.align(Alignment.CenterEnd)
.requiredSize(48.dp).padding(16.dp)
.clickable { showPassword = !showPassword }
)
}
}
)
}
// [END android_compose_text_showhidepassword]

// [START android_compose_text_auto_format_phone_number_validatetext]
class EmailViewModel : ViewModel() {
var email by mutableStateOf("")
Expand Down Expand Up @@ -954,10 +1157,64 @@ fun ValidatingInputTextField(
@Composable
fun ValidateInput() {
val emailViewModel: EmailViewModel = viewModel<EmailViewModel>()
ValidatingInputTextField(
ValidatingInputTextField1(
email = emailViewModel.email,
updateState = { input -> emailViewModel.updateEmail(input) },
validatorHasErrors = emailViewModel.emailHasErrors
)
}
// [END android_compose_text_auto_format_phone_number_validatetext]

// [START android_compose_text_state_auto_format_phone_number_validatetext]
class EmailViewModel1 : ViewModel() {
var email by mutableStateOf("")
private set

val emailHasErrors by derivedStateOf {
if (email.isNotEmpty()) {
// Email is considered erroneous until it completely matches EMAIL_ADDRESS.
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
} else {
false
}
}

fun updateEmail(input: String) {
email = input
}
}

@Composable
fun ValidatingInputTextField1(
email: String,
updateState: (String) -> Unit,
validatorHasErrors: Boolean
) {
// TODO: update
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
value = email,
onValueChange = updateState,
label = { Text("Email") },
isError = validatorHasErrors,
supportingText = {
if (validatorHasErrors) {
Text("Incorrect email format.")
}
}
)
}

@Preview
@Composable
fun ValidateInput1() {
val emailViewModel: EmailViewModel = viewModel<EmailViewModel>()
ValidatingInputTextField(
email = emailViewModel.email,
updateState = { input -> emailViewModel.updateEmail(input) },
validatorHasErrors = emailViewModel.emailHasErrors
)
}
// [END android_compose_text_state_auto_format_phone_number_validatetext]
9 changes: 5 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[versions]
accompanist = "0.36.0"
androidGradlePlugin = "8.8.1"
androidx-activity-compose = "1.10.0"
androidx-activity-compose = "1.10.1"
androidx-appcompat = "1.7.0"
androidx-compose-bom = "2025.02.00"
androidx-compose-bom = "2025.03.01"
androidx-compose-ui-test = "1.7.0-alpha08"
androidx-constraintlayout = "2.2.0"
androidx-constraintlayout-compose = "1.1.0"
Expand Down Expand Up @@ -40,7 +40,8 @@ kotlin = "2.1.10"
kotlinxSerializationJson = "1.8.0"
ksp = "2.1.10-1.0.30"
maps-compose = "6.4.4"
material = "1.13.0-alpha10"
material = "1.13.0-alpha12"
material3-alpha = "1.4.0-alpha11"
material3-adaptive = "1.1.0"
material3-adaptive-navigation-suite = "1.3.1"
media3 = "1.5.1"
Expand Down Expand Up @@ -73,7 +74,7 @@ androidx-compose-foundation-layout = { module = "androidx.compose.foundation:fou
androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" }
androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "compose-latest" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3-alpha" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" }
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" }
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" }
Expand Down
Loading