I am learning State hosting in jetpack compose. I am trying to separate my variable in single function and view logic to separate function. But I am getting weird issue in my code. Can someone guide me on this?
PulsePressure
#Composable
fun PulsePressure() {
var systolicTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
}
var isSystolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
var diastolicTextFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
}
var isDiastolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
InputWithUnitContainer(
systolicTextFieldValue,
isError = {
isSystolicTextFieldValueError = it
},
incrementTextFieldValue = {
systolicTextFieldValue = it
})
InputWithUnitContainer(
diastolicTextFieldValue,
isError = {
isDiastolicTextFieldValueError = it
},
incrementTextFieldValue = {
diastolicTextFieldValue = it
}
)
}
InputWithUnitContainer
#Composable
fun InputWithUnitContainer(
textFieldValue: TextFieldValue,
isError: (Boolean) -> Unit,
incrementTextFieldValue: (TextFieldValue) -> Unit,
) {
val maxLength = 4
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = textFieldValue,
singleLine = true,
onValueChange = {
if (it.text.length <= maxLength) {
incrementTextFieldValue(it)
}
isError(false)
},
isError = isError,
textStyle = RegularSlate20
)
}
}
Error on Textfield
None of the following functions can be called with the arguments supplied.
isError parameter in TextField is a Boolean.
Change 1
You have to change
var isDiastolicTextFieldValueError by rememberSaveable { mutableStateOf(false) }
to
var (isDiastolicTextFieldValueError, updateIsDiastolicTextFieldValueError) = rememberSaveable {
mutableStateOf(
false
)
}
This gives the value and a function to update the value.
Change 2
Then pass both the value and the method to the composable.
Usage
InputWithUnitContainer(
textFieldValue = diastolicTextFieldValue,
isError = isDiastolicTextFieldValueError,
updateIsError = updateIsDiastolicTextFieldValueError,
incrementTextFieldValue = {
diastolicTextFieldValue = it
}
)
Method signature change
#Composable
fun InputWithUnitContainer(
textFieldValue: TextFieldValue,
isError: Boolean,
updateIsError: (Boolean) -> Unit,
incrementTextFieldValue: (TextFieldValue) -> Unit,
) {
...
}
Change 3
Update the usage
TextField(
value = textFieldValue,
singleLine = true,
onValueChange = {
if (it.text.length <= maxLength) {
incrementTextFieldValue(it)
}
updateIsError(false)
},
isError = isError,
textStyle = TextStyle(),
)
Related
I'm trying to implement ExposedDropdownMenu - which I want to be displayed underneath TextField - when I set height of dropdown to max. 20 dp then everything is okay. But for any greater value it is always displayed above. Do you know what could be the issue here?
How it looks like:
My code:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun TextFieldWithDropdown(
modifier: Modifier = Modifier,
textFieldState: TextFieldState,
textCallback: (String) -> Unit,
list: List<String>,
keyboardOptions: KeyboardOptions,
textStyle: TextStyle
) {
// .align(Alignment.CenterStart)
val dropDownOptions = remember { mutableStateOf(listOf<String>()) }
val textFieldValue = remember { mutableStateOf(TextFieldValue()) }
val dropDownExpanded = remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = modifier,
expanded = dropDownExpanded.value, onExpandedChange = {
dropDownExpanded.value = !dropDownExpanded.value
}) {
TextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
.onFocusChanged { focusState ->
if (!focusState.isFocused)
dropDownExpanded.value = false
},
value = textFieldState.text.value,
onValueChange = {
textFieldState.text.value = it
textCallback.invoke(it)
dropDownOptions.value =
list.filter { it.startsWith(textFieldState.text.value) && it != textFieldState.text.value }
.take(3)
dropDownExpanded.value = dropDownOptions.value.isNotEmpty()
},
singleLine = true,
maxLines = 1,
textStyle = textStyle,
)
ExposedDropdownMenu(
expanded = dropDownExpanded.value,
onDismissRequest = {
dropDownExpanded.value = false
},
) {
dropDownOptions.value.forEach { text ->
DropdownMenuItem(text = {
Text(text = text)
}, onClick = {
textFieldState.text.value = text
dropDownExpanded.value = false
})
}
}
}
}
I am using Jetpack Compose, and it has occurred to me that I may be doing this incorrectly.
Suppose we have a screen that allows us to edit a form's data, which has been saved locally using Room. Currently, I follow this rough outline:
In my ViewModel's init block, call repository methods to query local Room Db and collect the results as a flow. Upon the flow change, update the ui state (which is a mutableStateOf inside of the viewModel and observed in the UI).
Now, I am following MVVM and my compose ui pattern is as follows: NavHost -> MyComposableScreen -> MyComposablePage. So we have:
#Composable
fun EditFormScreen(
viewModel: EditFormScreenViewModel,
onBackClick: () -> Unit,
onDoneClick: () -> Unit,
) {
val uiState = viewModel.uiState
LaunchedEffect(key1 = uiState) {
when (uiState.validationEvent) {
is FormValidationEvent.Initial -> {
// do nothing
}
is FormValidationEvent.Success -> {
onDoneClick()
}
}
}
Scaffold(
topBar = {
AppBar(
title = {
Text(
text = if (viewModel.id == null) {
stringResource(id = R.string.add_new_title)
} else {
stringResource(id = R.string.edit_existing_title)
},
)
},
onBackPressed = onBackClick,
)
}
) {
EditFormPage(
uiState = uiState,
onEvent = viewModel::onEvent,
)
}
}
fun EditFormPage(
uiState: EditFormPageUiState,
onEvent: (EditFormUiEvent) -> Unit = {},
) {
Column(
modifier = Modifier
...
) {
Column(
modifier = Modifier
...
) {
when(uiState.formLoadedState) {
FormLoadedState.Initial -> {
OutlinedInput(
label = stringResource(id = R.string.first_name),
onTextChanged = {
onEvent(
EditFormUiEvent.OnFirstNameChanged(it)
)
},
isError = uiState.isFirstNameError,
onNext = { focusManager.moveFocus(FocusDirection.Down) },
onDone = {},
)
OutlinedInput(
label = stringResource(id = R.string.last_name),
onTextChanged = {
onEvent(
EditFormUiEvent.OnLastNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.password),
onTextChanged = {
onEvent(
EditFormUiEvent.OnPasswordChanged(it)
)
},
...
)
}
FormLoadedState.Loading -> {
LoadingScreen()
}
is FormLoadedState.Success -> {
OutlinedInput(
label = stringResource(id = R.string.first_name),
initialValue = uiState.formLoadedState.user.firstName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnFirstNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.last_name),
initialValue = uiState.formLoadedState.user.lastName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnLastNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.password),
initialValue = uiState.formLoadedState.user.password,
onTextChanged = {
onEvent(
EditFormUiEvent.OnPasswordChanged(it)
)
},
...
)
}
}
}
MainButton(
label = stringResource(id = R.string.main_button_done),
onClick = {
focusManager.clearFocus()
onEvent(EditFormUiEvent.OnDoneClick)
}
)
}
}
My OutlinedInput composable is just a wrapper around OutlinedTextField, and is as follows:
#Composable
fun OutlinedInput(
modifier: ...,
label: String,
initialValue: String? = null,
textStyle: ...,
onTextChanged: (String) -> Unit,
isError: Boolean = false,
...
) {
var text by rememberSaveable { mutableStateOf(initialValue ?: "") }
OutlinedTextField(
modifier = modifier,
value = text,
onValueChange = {
text = it
onTextChanged(it)
},
isError = isError,
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(
onNext = onNext,
onDone = onDone,
),
textStyle = textStyle,
label = {
Text(
text = label
)
},
)
}
And finally my viewmodel class:
class EditFormScreenViewModel(
application: Application,
val id: Int? = null,
private val userRepository: UserRepository,
private val coroutineContextProvider: CoroutineContextProvider,
) : AndroidViewModel(application) {
var uiState: EditFormPageUiState by mutableStateOf(
EditFormPageUiState()
)
init {
if (id == null) {
// we are creating a new user
uiState = uiState.copy(
user = User(
...
)
)
} else {
// collect user flow to pre-populate UI fields
viewModelScope.launch {
uiState = uiState
.copy(
formLoadedState = FormLoadedState.Loading
)
withContext(coroutineContextProvider.IO) {
collectGetUserByIdFlow(id)
}
}
}
}
private suspend fun collectGetUserByIdFlow(id: Int) {
userRepository.getUserById(id = id)
.stateIn(viewModelScope)
.collectLatest(::onGetUserByIdUpdate)
}
private suspend fun onGetUserByIdUpdate(user: User) {
withContext(coroutineContextProvider.Main) {
uiState = uiState.copy(
formLoadedState = FormLoadedState.Success(
user = user
)
)
}
}
/**
* Manages user form input event & validation
*/
fun onEvent(uiEvent: EditFormUiEvent) {
when (uiEvent) {
is EditFormUiEvent.Initial -> {
// do nothing
}
is EditFormUiEvent.OnFirstNameChanged -> {
...
}
...
is EditFormUiEvent.OnDoneClick -> {
validateInputs()
}
}
}
private fun validateInputs() {
...
val hasError = listOf(
firstNameResult,
lastNameResult,
passwordResult,
).any { !it.status }
if(!hasError) {
viewModelScope.launch {
upsertUser(user)
}
}
}
}
private suspend fun upsertUser(user: User) {
userRepository.upsertUser(user = user)
withContext(coroutineContextProvider.Main) {
uiState = uiState.copy(
validationEvent = EditFormUiEvent.Success
)
}
}
}
The above works completely as expected: Arrive at screen -> init view model loads data -> while data is loading shows a progress bar -> when data is done loading, ui state is updated to success and the data is preloaded into the form.
However, I can't help but feel like I am missing a simpler way to achieve this and avoid the repetition in the EditFormPage composable, specifically, referring to this part:
when(uiState.formLoadedState) {
FormLoadedState.Initial -> {
OutlinedInput(
label = stringResource(id = R.string.first_name),
onTextChanged = {
onEvent(
EditFormUiEvent.OnFirstNameChanged(it)
)
},
isError = uiState.isFirstNameError,
onNext = { focusManager.moveFocus(FocusDirection.Down) },
onDone = {},
)
OutlinedInput(
label = stringResource(id = R.string.last_name),
onTextChanged = {
onEvent(
EditFormUiEvent.OnLastNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.password),
onTextChanged = {
onEvent(
EditFormUiEvent.OnPasswordChanged(it)
)
},
...
)
}
FormLoadedState.Loading -> {
LoadingScreen()
}
is FormLoadedState.Success -> {
OutlinedInput(
label = stringResource(id = R.string.first_name),
initialValue = uiState.formLoadedState.user.firstName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnFirstNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.last_name),
initialValue = uiState.formLoadedState.user.lastName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnLastNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.password),
initialValue = uiState.formLoadedState.user.password,
onTextChanged = {
onEvent(
EditFormUiEvent.OnPasswordChanged(it)
)
},
...
)
}
}
}
...
How can I, taking my current structure into account, achieve something where my edit form page instead looks like this? (i.e.: no initial/loading/success states):
OutlinedInput(
label = stringResource(id = R.string.first_name),
initialValue = uiState.user.firstName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnFirstNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.last_name),
initialValue = uiState.user.lastName,
onTextChanged = {
onEvent(
EditFormUiEvent.OnLastNameChanged(it)
)
},
...
)
OutlinedInput(
label = stringResource(id = R.string.password),
initialValue = uiState.user.password,
onTextChanged = {
onEvent(
EditFormUiEvent.OnPasswordChanged(it)
)
},
...
)
I would expect the above to work, since initial value in the OutlinedInput can use something uiState.user.firstName, and I would think that once I do this in the viewmodel:
private suspend fun onGetUserByIdUpdate(user: User) {
withContext(coroutineContextProvider.Main) {
uiState = uiState.copy(
user = user
)
}
}
The OutlinedInput would recompose, and display the updated uiState's user's data. However, this doesn't happen.
Please check below code and I think this will help you. A similar implementation
val fullNameText = remember { mutableStateOf("") }
fullNameText.value = state.user.fullName
TextField(
label = stringResource(id = R.string.fullname),
textValue = fullNameText.value,
onValueChange = { newText ->
fullNameText.value = newText.trim()
onEvent(ProfileEditEvent.SetFullName( newText.trim()))
}
)
Here we are assaigning this into a mutableState rememberable then set state to it and then reassign. I think somebody will suggest you a better option.
I have a drop down spinner where i am able to get the category_title . how to get selected category_id i have created a two mutableStateOf of selected title and Id .is there any other way to get the selected category id
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
data class
data class MainCategory(
var category_id: String? = null,
var category_name: String? = null,
var categoryImage_url: String? = null,
var category_priority:Int? = null
)
list variable
val mCategories = mainCategories.mapTo(arrayListOf()){it.category_name}
DropDownMenu
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun MainCatDownloadContent(
mainCategories: Categories,
mainCategoryOnClick: (categoryId: String, categoryTitle:
String) -> Unit,
) {
var mExpanded by remember { mutableStateOf(false) }
val mCategories = mainCategories.mapTo(arrayListOf()){it.category_name}
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
var mTextFieldSize by remember { mutableStateOf(Size.Zero) }
// Up Icon when expanded and down icon when collapsed
val icon = if (mExpanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mSelectedText, onValueChange = { mSelectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory")},
trailingIcon = {
Icon(icon,"contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
mCategories.forEach{ mainCategory ->
DropdownMenuItem(onClick = {
mSelectedText = mainCategory.toString()
mExpanded = false }) {
Text(text = mainCategory.toString())
}
}
}
}
For ex if there is list of items like this
[MainCategory(category_id=11, category_name=Fruits,
categoryImage_url=https:www category_priority=1),
MainCategory(category_id=22, category_name=Vegetable ,
categoryImage_url=https:www, category_priority=2),
MainCategory(category_id=33, category_name=Greens,
categoryImage_url=https:www, category_priority=3)
if selected item lets assume is "Fruits"! how to get its category_id which is "11" and assign to the mSelectedCategoryId variable
as #Gabriele Mariotti says you need to create rememberSaveable State Just elaborating his answer
I assume mainCategories: Categories
// is a List<> so State you can by
val selectCategory by rememberSaveable {
mutableStateOf(mainCategories)
}
Other two variables which you have
var mSelectedText by remember { mutableStateOf("") }
var mSelectedCategoryId by remember { mutableStateOf("") }
Your are missing the DropDownMenu and i assume you just need to show the categoryName and get the selected id of the drop down menu from your question you can do this way
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mSelectedText,
onValueChange = { mSelectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory") },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
DropdownMenu(expanded = mExpanded,
onDismissRequest = { mExpanded = false },
modifier = Modifier.width(with(
LocalDensity.current) {
mTextFieldSize.width.toDp()
})) {
selectCategory.forEach {
DropdownMenuItem(onClick = {
mSelectedText = it.category_name.toString()
mSelectedCategoryId = it.category_id.toString()
mExpanded = false
}) {
Text(text = it.category_name.toString())
}
}
}
}
Log.i(TAG,"Get the category_name: $mSelectedText and category_id: $mSelectedCategoryId" )
You can use something different.
Add the #Parcelize annotation to the object:
#Parcelize
data class MainCategory(
var category_id: String,
var category_name: String,
) : Parcelable
Then you can use a list of MainCategory and save the state of the selected MainCategory using:
#Composable
fun MainCatDownloadContent(
mainCategories: List<MainCategory>
){
var mExpanded by remember { mutableStateOf(false) }
var selectedCategory by rememberSaveable {
mutableStateOf(mainCategories[0])
}
//...
OutlinedTextField(
value = value = selectedCategory.category_name + " - " + selectedCategory.category_id, //you can access all the properties inside the selectedCategory
//..
)
//..
mainCategories.forEach{ mainCategory ->
DropdownMenuItem(
onClick = {
selectedCategory = mainCategory
mExpanded = false
}
){
//....
}
}
}
Finally in your case is better to use a ExposedDropdownMenuBox + ExposedDropdownMenu as described in this question.
I'm writing instrumented tests for a Jetpack Compose component. My composable uses rememberSaveable to remember between configuration changes (activity restarts):
#Composable
fun AddUserScreen() {
Input(
shouldRequestFocus = true,
stringResource(R.string.user_first_name),
stringResource(R.string.user_first_name_label),
tag = "input-first-name"
)
}
#Composable
fun Input(
shouldRequestFocus: Boolean,
text: String,
label: String,
tag: String
) {
var value by rememberSaveable { mutableStateOf("") } // <-- Important part
val focusRequester = FocusRequester()
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text)
Spacer(modifier = Modifier.width(10.dp))
TextField(
value = value,
onValueChange = { value = it },
label = { Text(label) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = Modifier
.focusRequester(focusRequester)
.testTag(tag)
)
}
if (shouldRequestFocus) {
DisposableEffect(Unit) {
focusRequester.requestFocus()
onDispose { }
}
}
}
The input value is retained when I open the app myself and rotate the device. But in the following test the input is not retained on configuration change and the test fails:
#get:Rule val composeTestRule = createAndroidComposeRule<AddUserActivity>()
#Test fun whenAConfigChangeHappensTheFirstNameInputShouldRetainItsValue() {
composeTestRule.setContent {
WorkoutLoggerTheme {
AddUserScreen()
}
}
composeTestRule.onNodeWithTag("input-first-name").performTextInput("John")
composeTestRule.activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("input-first-name").assertTextEquals("John")
}
I want to create textfield with hint text in jetpackcompose. Any example how create textfield using jectpack? Thanks
compose_version = '1.0.0-beta07'
Use parameter placeholder to show hint
TextField(value = "", onValueChange = {}, placeholder = { Text("Enter Email") })
Use parameter label to show floating label
TextField(value = "", onValueChange = {}, label = { Text("Enter Email") })
You can use
the label parameter in the TextField composable to display a floating label
the placeholder parameter to display a placeholder when the text field is in focus and the input text is empty.
You can also use them together.
Something like:
var text by remember { mutableStateOf("text") }
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
label = {
Text("Label")
}
)
or
TextField(
value = text,
onValueChange = {
text = it
},
label = {
Text("Label")
},
placeholder = {
Text("Placeholder")
}
)
You can create hintTextField in jetpackCompose like below code:
#Composable
fun HintEditText(hintText: #Composable() () -> Unit) {
val state = state { "" } // The unary plus is no longer needed. +state{""}
val inputField = #Composable {
TextField(
value = state.value,
onValueChange = { state.value = it }
)
}
if (state.value.isNotEmpty()) {
inputField()
} else {
Layout(inputField, hintText) { measurable, constraints ->
val inputfieldPlacable = measurable[inputField].first().measure(constraints)
val hintTextPlacable = measurable[hintText].first().measure(constraints)
layout(inputfieldPlacable.width, inputfieldPlacable.height) {
inputfieldPlacable.place(0.ipx, 0.ipx)
hintTextPlacable.place(0.ipx, 0.ipx)
} }
}
}
Call #Compose function like below:
HintEditText #Composable {
Text(
text = "Enter Email",
style = TextStyle(
color = Color.White,
fontSize = 18.sp
)
)
}
Jetpack compose version: dev08
The benefit of compose is that we can easily create our widgets by composing current composable functions.
We can just create a function with all parameters of the current TextField and add a
hint: String parameter.
#Composable
fun TextFieldWithHint(
value: String,
modifier: Modifier = Modifier.None,
hint: String,
onValueChange: (String) -> Unit,
textStyle: TextStyle = currentTextStyle(),
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null,
onTextLayout: (TextLayoutResult) -> Unit = {}
) {
Stack(Modifier.weight(1f)) {
TextField(value = value,
modifier = modifier,
onValueChange = onValueChange,
textStyle = textStyle,
keyboardType = keyboardType,
imeAction = imeAction,
onFocus = onFocus,
onBlur = onBlur,
focusIdentifier = focusIdentifier,
onImeActionPerformed = onImeActionPerformed,
visualTransformation = visualTransformation,
onTextLayout = onTextLayout)
if (value.isEmpty()) Text(hint)
}
}
We can use it like this:
#Model
object model { var text: String = "" }
TextFieldWithHint(value = model.text, onValueChange = { data -> model.text = data },
hint= "Type book name or author")
The pitfall of this approach is we are passing the hint as a string so if we want to style the hint we should add extra parameters to the TextFieldWithHint (e.g hintStyle, hintModifier, hintSoftWrap, ...)
The better approach is to accept a composable lambda instead of string:
#Composable
fun TextFieldWithHint(
value: String,
modifier: Modifier = Modifier.None,
hint: #Composable() () -> Unit,
onValueChange: (String) -> Unit,
textStyle: TextStyle = currentTextStyle(),
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null,
onTextLayout: (TextLayoutResult) -> Unit = {}
) {
Stack(Modifier.weight(1f)) {
TextField(value = value,
modifier = modifier,
onValueChange = onValueChange,
textStyle = textStyle,
keyboardType = keyboardType,
imeAction = imeAction,
onFocus = onFocus,
onBlur = onBlur,
focusIdentifier = focusIdentifier,
onImeActionPerformed = onImeActionPerformed,
visualTransformation = visualTransformation,
onTextLayout = onTextLayout)
if (value.isEmpty()) hint()
}
}
We can use it like this:
#Model
object model { var text: String = "" }
TextFieldWithHint(value = model.text, onValueChange = { data -> model.text = data },
hint= { Text("Type book name or author", style = TextStyle(color = Color(0xFFC7C7C7))) })
var textState by remember { mutableStateOf(TextFieldValue()) }
var errorState by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
TextField(
value = textState,
onValueChange = {
textState = it
when {
textState.text.isEmpty() -> {
errorState = true
errorMessage = "Please Enter Site Code"
}
else -> {
errorState = false
errorMessage = ""
}
}
},
isError = errorState,
label = {
Text(
text = if (errorState) errorMessage
else "You Hint"
)
},
modifier = Modifier
.padding(top = 20.dp, start = 30.dp, end = 30.dp)
.fillMaxWidth())
If you want to create a text field with hint text and custom color here is the code for that.
#Composable
fun PhoneOrEmail() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Phone/email", style = TextStyle(color =
Color.Red)) }
)//End of TextField
}//End of Function
Here's what works for me (I think it's a bit simpler than what Anas posted since it's using the same component:
#Composable
fun TextBox(
loginInput: LoginInput,
hint: String = "enter value",
color: Color = Color.LightGray,
height: Dp = 50.dp
) {
val state = +state { "" }
state.value = if (loginInput.usernameEntered) loginInput.username else hint
Surface(color = color) {
Row {
Container(modifier = Expanded, height = height) {
Clip(shape = RoundedCornerShape(15.dp)) {
Padding(padding = 15.dp) {
TextField(
value = state.value,
keyboardType = KeyboardType.Text,
onFocus = {
if (!loginInput.usernameEntered)
state.value = ""
},
onValueChange = {
loginInput.usernameEntered = true
loginInput.username = it
state.value = loginInput.username
}
)
}
}
}
}
}
}
the label parameter will be displayed as text if text is empty and moves above the textfield (as label) on typing input:
#Composable
fun SearchField() {
val (text, setText) = remember { mutableStateOf(TextFieldValue("")) }
Box(modifier = Modifier.width(180.dp).padding(2.dp)) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = { setText(it) },
label = { Text("quick do:") },
)
}
}