Skip to content

Commit 4c55c0d

Browse files
Update migration snippet (#524)
* Adding TextField migration snippets This is for the new version of https://developer.android.com/develop/ui/compose/text/user-input * Update TextFieldMigrationSnippets.kt optimize import * Apply Spotless --------- Co-authored-by: Halil Ozercan <[email protected]>
1 parent cd2e0c1 commit 4c55c0d

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.compose.snippets.text
18+
19+
import androidx.compose.foundation.layout.Column
20+
import androidx.compose.foundation.text.input.InputTransformation
21+
import androidx.compose.foundation.text.input.OutputTransformation
22+
import androidx.compose.foundation.text.input.TextFieldLineLimits
23+
import androidx.compose.foundation.text.input.TextFieldState
24+
import androidx.compose.foundation.text.input.delete
25+
import androidx.compose.foundation.text.input.insert
26+
import androidx.compose.foundation.text.input.maxLength
27+
import androidx.compose.foundation.text.input.rememberTextFieldState
28+
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
29+
//noinspection UsingMaterialAndMaterial3Libraries
30+
import androidx.compose.material.SecureTextField
31+
//noinspection UsingMaterialAndMaterial3Libraries
32+
import androidx.compose.material.Text
33+
//noinspection UsingMaterialAndMaterial3Libraries
34+
import androidx.compose.material.TextField
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.LaunchedEffect
37+
import androidx.compose.runtime.getValue
38+
import androidx.compose.runtime.mutableStateOf
39+
import androidx.compose.runtime.remember
40+
import androidx.compose.runtime.saveable.rememberSaveable
41+
import androidx.compose.runtime.setValue
42+
import androidx.compose.runtime.snapshotFlow
43+
import androidx.compose.ui.Modifier
44+
import androidx.compose.ui.text.AnnotatedString
45+
import androidx.compose.ui.text.TextRange
46+
import androidx.compose.ui.text.input.OffsetMapping
47+
import androidx.compose.ui.text.input.PasswordVisualTransformation
48+
import androidx.compose.ui.text.input.TextFieldValue
49+
import androidx.compose.ui.text.input.TransformedText
50+
import androidx.compose.ui.text.input.VisualTransformation
51+
import androidx.compose.ui.text.substring
52+
import androidx.compose.ui.tooling.preview.Preview
53+
import androidx.lifecycle.ViewModel
54+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
55+
import com.example.compose.snippets.touchinput.Button
56+
import kotlinx.coroutines.flow.MutableStateFlow
57+
import kotlinx.coroutines.flow.StateFlow
58+
import kotlinx.coroutines.flow.asStateFlow
59+
import kotlinx.coroutines.flow.collectLatest
60+
import kotlinx.coroutines.flow.update
61+
62+
// [START android_compose_text_textfield_migration_old_simple]
63+
@Composable
64+
fun OldSimpleTextField() {
65+
var state by rememberSaveable { mutableStateOf("") }
66+
TextField(
67+
value = state,
68+
onValueChange = { state = it },
69+
singleLine = true,
70+
)
71+
}
72+
// [END android_compose_text_textfield_migration_old_simple]
73+
74+
// [START android_compose_text_textfield_migration_new_simple]
75+
@Composable
76+
fun NewSimpleTextField() {
77+
TextField(
78+
state = rememberTextFieldState(),
79+
lineLimits = TextFieldLineLimits.SingleLine
80+
)
81+
}
82+
// [END android_compose_text_textfield_migration_new_simple]
83+
84+
// [START android_compose_text_textfield_migration_old_filtering]
85+
@Composable
86+
fun OldNoLeadingZeroes() {
87+
var input by rememberSaveable { mutableStateOf("") }
88+
TextField(
89+
value = input,
90+
onValueChange = { newText ->
91+
input = newText.trimStart { it == '0' }
92+
}
93+
)
94+
}
95+
// [END android_compose_text_textfield_migration_old_filtering]
96+
97+
// [START android_compose_text_textfield_migration_new_filtering]
98+
99+
@Preview
100+
@Composable
101+
fun NewNoLeadingZeros() {
102+
TextField(
103+
state = rememberTextFieldState(),
104+
inputTransformation = InputTransformation {
105+
while (length > 0 && charAt(0) == '0') delete(0, 1)
106+
}
107+
)
108+
}
109+
// [END android_compose_text_textfield_migration_new_filtering]
110+
111+
// [START android_compose_text_textfield_migration_old_credit_card_formatter]
112+
@Composable
113+
fun OldTextFieldCreditCardFormatter() {
114+
var state by remember { mutableStateOf("") }
115+
TextField(
116+
value = state,
117+
onValueChange = { if (it.length <= 16) state = it },
118+
visualTransformation = VisualTransformation { text ->
119+
// Making XXXX-XXXX-XXXX-XXXX string.
120+
var out = ""
121+
for (i in text.indices) {
122+
out += text[i]
123+
if (i % 4 == 3 && i != 15) out += "-"
124+
}
125+
126+
TransformedText(
127+
text = AnnotatedString(out),
128+
offsetMapping = object : OffsetMapping {
129+
override fun originalToTransformed(offset: Int): Int {
130+
if (offset <= 3) return offset
131+
if (offset <= 7) return offset + 1
132+
if (offset <= 11) return offset + 2
133+
if (offset <= 16) return offset + 3
134+
return 19
135+
}
136+
137+
override fun transformedToOriginal(offset: Int): Int {
138+
if (offset <= 4) return offset
139+
if (offset <= 9) return offset - 1
140+
if (offset <= 14) return offset - 2
141+
if (offset <= 19) return offset - 3
142+
return 16
143+
}
144+
}
145+
)
146+
}
147+
)
148+
}
149+
// [END android_compose_text_textfield_migration_old_credit_card_formatter]
150+
151+
// [START android_compose_text_textfield_migration_new_credit_card_formatter]
152+
@Composable
153+
fun NewTextFieldCreditCardFormatter() {
154+
val state = rememberTextFieldState()
155+
TextField(
156+
state = state,
157+
inputTransformation = InputTransformation.maxLength(16),
158+
outputTransformation = OutputTransformation {
159+
if (length > 4) insert(4, "-")
160+
if (length > 9) insert(9, "-")
161+
if (length > 14) insert(14, "-")
162+
},
163+
)
164+
}
165+
// [END android_compose_text_textfield_migration_new_credit_card_formatter]
166+
167+
private object StateUpdateSimpleSnippet {
168+
object UserRepository {
169+
suspend fun fetchUsername(): String = TODO()
170+
}
171+
// [START android_compose_text_textfield_migration_old_update_state_simple]
172+
@Composable
173+
fun OldTextFieldStateUpdate(userRepository: UserRepository) {
174+
var username by remember { mutableStateOf("") }
175+
LaunchedEffect(Unit) {
176+
username = userRepository.fetchUsername()
177+
}
178+
TextField(
179+
value = username,
180+
onValueChange = { username = it }
181+
)
182+
}
183+
// [END android_compose_text_textfield_migration_old_update_state_simple]
184+
185+
// [START android_compose_text_textfield_migration_new_update_state_simple]
186+
@Composable
187+
fun NewTextFieldStateUpdate(userRepository: UserRepository) {
188+
val usernameState = rememberTextFieldState()
189+
LaunchedEffect(Unit) {
190+
usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername())
191+
}
192+
TextField(state = usernameState)
193+
}
194+
// [END android_compose_text_textfield_migration_new_update_state_simple]
195+
}
196+
197+
// [START android_compose_text_textfield_migration_old_state_update_complex]
198+
@Composable
199+
fun OldTextFieldAddMarkdownEmphasis() {
200+
var markdownState by remember { mutableStateOf(TextFieldValue()) }
201+
Button(onClick = {
202+
// add ** decorations around the current selection, also preserve the selection
203+
markdownState = with(markdownState) {
204+
copy(
205+
text = buildString {
206+
append(text.take(selection.min))
207+
append("**")
208+
append(text.substring(selection))
209+
append("**")
210+
append(text.drop(selection.max))
211+
},
212+
selection = TextRange(selection.min + 2, selection.max + 2)
213+
)
214+
}
215+
}) {
216+
Text("Bold")
217+
}
218+
TextField(
219+
value = markdownState,
220+
onValueChange = { markdownState = it },
221+
maxLines = 10
222+
)
223+
}
224+
// [END android_compose_text_textfield_migration_old_state_update_complex]
225+
226+
// [START android_compose_text_textfield_migration_new_state_update_complex]
227+
@Composable
228+
fun NewTextFieldAddMarkdownEmphasis() {
229+
val markdownState = rememberTextFieldState()
230+
LaunchedEffect(Unit) {
231+
// add ** decorations around the current selection
232+
markdownState.edit {
233+
insert(originalSelection.max, "**")
234+
insert(originalSelection.min, "**")
235+
selection = TextRange(originalSelection.min + 2, originalSelection.max + 2)
236+
}
237+
}
238+
TextField(
239+
state = markdownState,
240+
lineLimits = TextFieldLineLimits.MultiLine(1, 10)
241+
)
242+
}
243+
// [END android_compose_text_textfield_migration_new_state_update_complex]
244+
245+
private object ViewModelMigrationOldSnippet {
246+
// [START android_compose_text_textfield_migration_old_viewmodel]
247+
class LoginViewModel : ViewModel() {
248+
private val _uiState = MutableStateFlow(UiState())
249+
val uiState: StateFlow<UiState>
250+
get() = _uiState.asStateFlow()
251+
252+
fun updateUsername(username: String) = _uiState.update { it.copy(username = username) }
253+
254+
fun updatePassword(password: String) = _uiState.update { it.copy(password = password) }
255+
}
256+
257+
data class UiState(
258+
val username: String = "",
259+
val password: String = ""
260+
)
261+
262+
@Composable
263+
fun LoginForm(
264+
loginViewModel: LoginViewModel,
265+
modifier: Modifier = Modifier
266+
) {
267+
val uiState by loginViewModel.uiState.collectAsStateWithLifecycle()
268+
Column(modifier) {
269+
TextField(
270+
value = uiState.username,
271+
onValueChange = { loginViewModel.updateUsername(it) }
272+
)
273+
TextField(
274+
value = uiState.password,
275+
onValueChange = { loginViewModel.updatePassword(it) },
276+
visualTransformation = PasswordVisualTransformation()
277+
)
278+
}
279+
}
280+
// [END android_compose_text_textfield_migration_old_viewmodel]
281+
}
282+
283+
private object ViewModelMigrationNewSimpleSnippet {
284+
// [START android_compose_text_textfield_migration_new_viewmodel_simple]
285+
class LoginViewModel : ViewModel() {
286+
val usernameState = TextFieldState()
287+
val passwordState = TextFieldState()
288+
}
289+
290+
@Composable
291+
fun LoginForm(
292+
loginViewModel: LoginViewModel,
293+
modifier: Modifier = Modifier
294+
) {
295+
Column(modifier) {
296+
TextField(state = loginViewModel.usernameState,)
297+
SecureTextField(state = loginViewModel.passwordState)
298+
}
299+
}
300+
// [END android_compose_text_textfield_migration_new_viewmodel_simple]
301+
}
302+
303+
private object ViewModelMigrationNewConformingSnippet {
304+
// [START android_compose_text_textfield_migration_new_viewmodel_conforming]
305+
class LoginViewModel : ViewModel() {
306+
private val _uiState = MutableStateFlow(UiState())
307+
val uiState: StateFlow<UiState>
308+
get() = _uiState.asStateFlow()
309+
310+
fun updateUsername(username: String) = _uiState.update { it.copy(username = username) }
311+
312+
fun updatePassword(password: String) = _uiState.update { it.copy(password = password) }
313+
}
314+
315+
data class UiState(
316+
val username: String = "",
317+
val password: String = ""
318+
)
319+
320+
@Composable
321+
fun LoginForm(
322+
loginViewModel: LoginViewModel,
323+
modifier: Modifier = Modifier
324+
) {
325+
val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value }
326+
Column(modifier) {
327+
val usernameState = rememberTextFieldState(initialUiState.username)
328+
LaunchedEffect(usernameState) {
329+
snapshotFlow { usernameState.text.toString() }.collectLatest {
330+
loginViewModel.updateUsername(it)
331+
}
332+
}
333+
TextField(usernameState)
334+
335+
val passwordState = rememberTextFieldState(initialUiState.password)
336+
LaunchedEffect(usernameState) {
337+
snapshotFlow { usernameState.text.toString() }.collectLatest {
338+
loginViewModel.updatePassword(it)
339+
}
340+
}
341+
SecureTextField(passwordState)
342+
}
343+
}
344+
// [END android_compose_text_textfield_migration_new_viewmodel_conforming]
345+
}

0 commit comments

Comments
 (0)