android modern development: how can I understand user did not change any data in a screen? - kotlin

I built a fragment, using jetpack compose for adding views on screen and state and view model classes for saving state.
first time when I navigate to this fragment, fetch API is called and If I have any value on this API, I filled text fields with them. It is possible that the API is empty and do not return any value.
Users can enter any data on this text fields or can ignore them because filling the text fields is optional and when he/she clicks on submit button; the data is saved in server by calling save API and shows a successful message that data is saved.
my question is that when user navigates to this fragment and does not enter or change any value,
when clicks on submit button, I do not want to show that toast.
how can I handle it? thanks.

First you will need to create a data class to hold the values for your screen as a mutable state with a default value.
data class SampleState(
val isLoading: Boolean = true,
val someText1: String = "",
val someText2: String = ""
)
It is also necessary to create a sealed class with the possible screen actions, such as changing someText1 and someText2 and also the click of a button.
sealed class SampleAction {
object TestButton : SampleAction()
data class ChangeSomeText1(val text1: String) : SampleAction()
data class ChangeSomeText2(val text2: String) : SampleAction()
}
And finally, a sealed class to contain the possible one-time-event that can be triggered through some action, like after clicking on the button with the fields properly filled in.
sealed class SampleChannel {
object ValidFields : SampleChannel()
}
The view model will be responsible for managing all the logic:
#HiltViewModel
class SampleViewModel #Inject constructor() : ViewModel() {
var state by mutableStateOf(value = SampleState())
private set
private val _channel = Channel<SampleChannel>()
val channel = _channel.receiveAsFlow()
init {
loadInitialFakeData()
}
fun onAction(action: SampleAction) {
when (action) {
is SampleAction.TestButton -> testButtonClicked()
is SampleAction.ChangeSomeText1 -> state = state.copy(someText1 = action.text1)
is SampleAction.ChangeSomeText2 -> state = state.copy(someText2 = action.text2)
}
}
private fun loadInitialFakeData() = viewModelScope.launch {
delay(timeMillis = 1000)
if (Random.nextBoolean()) state = state.copy(
someText1 = "fake text 1 from api",
someText2 = "fake text 2 from api"
)
state = state.copy(isLoading = false)
}
private fun testButtonClicked() {
val hasBlankField = listOf(
state.someText1,
state.someText2
).any { string ->
string.isBlank()
}
if (hasBlankField) return
viewModelScope.launch {
_channel.send(SampleChannel.ValidFields)
}
}
}
In this case there is a loadInitialFakeData function that is executed as soon as the view model is instantiated, which simulates a delay only for testing purposes and according to a random boolean assigns or not values in the state class.
There is also a function testButtonClicked that will do the logic if any of the fields is empty, if any of them is empty nothing happens (I left it that way so as not to extend the code too much) and if it is not empty we notify the channel with the ValidFields.
The only public function in the view model is the onAction, which is responsible for receiving actions through the composable screen.
Now on the composable screen we can do it like this:
#Composable
fun SampleScreen(
viewModel: SampleViewModel = hiltViewModel()
) {
val state = viewModel.state
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = Unit) {
viewModel.channel.collect { channel ->
when (channel) {
SampleChannel.ValidFields -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "all right!"
)
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Column(modifier = Modifier.fillMaxSize()) {
ComposeLoading(isLoading = state.isLoading)
ComposeForm(
changeSomeText1 = {
viewModel.onAction(SampleAction.ChangeSomeText1(it))
},
changeSomeText2 = {
viewModel.onAction(SampleAction.ChangeSomeText2(it))
},
testButtonClick = {
viewModel.onAction(SampleAction.TestButton)
},
state = state
)
}
}
}
#Composable
private fun ComposeLoading(
isLoading: Boolean
) = AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isLoading,
enter = expandVertically(animationSpec = tween()),
exit = shrinkVertically(animationSpec = tween())
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = "loading...",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
#Composable
private fun ComposeForm(
changeSomeText1: (String) -> Unit,
changeSomeText2: (String) -> Unit,
testButtonClick: () -> Unit,
state: SampleState
) = Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ComposeSimpleTextField(
value = state.someText1,
onValueChange = {
changeSomeText1(it)
},
label = "text field 1"
)
Spacer(modifier = Modifier.height(height = 16.dp))
ComposeSimpleTextField(
value = state.someText2,
onValueChange = {
changeSomeText2(it)
},
label = "text field 2"
)
Spacer(modifier = Modifier.height(height = 16.dp))
Button(
modifier = Modifier.align(alignment = Alignment.End),
onClick = testButtonClick
) {
Text(text = "Test Button")
}
}
#Composable
private fun ComposeSimpleTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) = TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
label = {
Text(text = label)
}
)
The main thing here is the LaunchedEffect to collect all possible events from the SampleChannel (on this example there is only one) and the calls to screen events when the text of some field is changed or when the button is clicked... The rest is just a compose boilerplate that doesn't matter much for the main logic.
Note: I used Hilt for dependency injection in this example, so I had access to #HiltViewModel and hiltViewModel()
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'

Related

How can I select only 3 element in a grid/list (jetpack compose)?

I have a Grid with two columns and 6 elements, but I need that the user select only 3 elements
In the first #Composable I put the value in this way:
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.values.size) {
InterestsItems(
value = list.values[it].text,
)
}
}
And the second #Composable(InterestsItems) is a Box with an Image inside. I put the value like this:
var isSelected by remember { mutableStateOf(false) }
Box(modifier = Modifier.noRippleClickable { isSelected = !isSelected })
The result is that I select all the elements, is not what I want.
You can create a data class to hold selected flag for any item
data class InterestsItem(val text: String, val isSelected: Boolean = false)
A ViewModel that keeps items and indexes of selected items and function to toggle between selected and not selected
class InterestsViewModel : ViewModel() {
val interestItems = mutableStateListOf<InterestsItem>()
.apply {
repeat(6) {
add(InterestsItem(text = "Item$it"))
}
}
private val selectedItems = mutableListOf<Int>()
fun toggleSelection(index: Int) {
val item = interestItems[index]
val isSelected = item.isSelected
if (isSelected) {
interestItems[index] = item.copy(isSelected = false)
selectedItems.remove(index)
} else if (selectedItems.size < 3) {
interestItems[index] = item.copy(isSelected = true)
selectedItems.add(index)
}
}
}
MutableStateListOf will trigger recomposition when we change item
#Composable
private fun SelectItemsFromGridSample(interestsViewModel: InterestsViewModel) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(interestsViewModel.interestItems) { index, item ->
InterestsItemCard(interestsItem = item) {
interestsViewModel.toggleSelection(index)
}
}
}
}
And some Composable with callback to pass that item is clicked
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun InterestsItemCard(interestsItem: InterestsItem, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier.size(100.dp),
onClick = onClick
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = interestsItem.text, fontSize = 30.sp)
if (interestsItem.isSelected) {
Icon(
modifier = Modifier
.size(50.dp)
.background(Color.Green, CircleShape),
imageVector = Icons.Default.Check,
tint = Color.White,
contentDescription = null
)
}
}
}
}
Result
You are apparently using a common isSelected variable for all the items. If you use a loop to render the items, move the variable declaration inside the loop.
Take the basic programming codelabs on the Android Developers Official Website, to walk through the basics. A strong base in general programming is foundational to learning about any specific programming paradigms/fields. Definitely take the Compose Pathway, now renamed to the Jetpack Compose Course, before anything. In this scenario, you'll need to create either a list representing the selected items, or a hash-map, which is basically a list in which each element is a pair of two objects. When the size of the list/map gets to 3, handle the event inside the on-click, to prevent further selection. That'll depend on what you want to do, and should be custom to your project.
Put the isSelected variable inside the items block.
Example;
val list = remember { mutableStateOf(listOf("0","1","2","3","4","5") )}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.value.size){
var isSelected by remember { mutableStateOf(false) }
Text(
modifier = Modifier.clickable{
isSelected = !isSelected
},
text = list.value[it],
color = if (isSelected) Color.Red else Color.Black
)
}
}
Result;

Jetpack compose custom snackbar material 3

How to achieve a custom snackbar in compose using material 3? I want to change the alignment of the snackbar. Also I want dynamic icon on the snackbar either on left or right side.
You can customize your snackbar using SnackBar composable and can change alignment using SnackbarHost alignment inside a Box if that's what you mean.
val snackState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
coroutineScope.launch {
snackState.showSnackbar("Custom Snackbar")
}
}) {
Text("Show Snackbar")
}
}
SnackbarHost(
modifier=Modifier.align(Alignment.BottomStart),
hostState = snackState
) { snackbarData: SnackbarData ->
CustomSnackBar(
R.drawable.baseline_swap_horiz_24,
snackbarData.visuals.message,
isRtl = true,
containerColor = Color.Gray
)
}
}
#Composable
fun CustomSnackBar(
#DrawableRes drawableRes: Int,
message: String,
isRtl: Boolean = true,
containerColor: Color = Color.Black
) {
Snackbar(containerColor = containerColor) {
CompositionLocalProvider(
LocalLayoutDirection provides
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
Row {
Icon(
painterResource(id = drawableRes),
contentDescription = null
)
Text(message)
}
}
}
}
Validated answer does not answer the question properly because the icon is provided in a static way, not dynamic. The icon is not passed to showSnackbar.
You can do it by having your own SnackbarVisuals :
// Your custom visuals
// Default values are the same than SnackbarHostState.showSnackbar
data class SnackbarVisualsCustom(
override val message: String,
override val actionLabel: String? = null,
override val withDismissAction: Boolean = false,
override val duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
// You can add custom things here (for you it's an icon)
#DrawableRes val drawableRes: Int
) : SnackbarVisuals
// The way you decide how to display your custom visuals
SnackbarHost(hostState = snackbarHostState) {
val customVisuals = it.visuals as SnackbarVisualsCustom
Snackbar {
// Here is your custom snackbar where you use your icon
}
}
// To display the custom snackbar
snackbarHostStateScope.launch {
snackbarHostState.showSnackbar(
SnackbarVisualsCustom(
message = "The message",
drawableRes = R.drawable.your_icon_id
)
)
}

Is a good way to use State<Boolean> in view model with Android Compose?

I have read some sample codes for learning Compose.
I find many sample projects use Code A to create a StateFlow in view model, then convert it to State in #Composable function, the UI will be updated automatically when drawerOpen is changed.
1: I think both Code B and Code C can do the same thing, right? Why does many projects seldom to use them?
2: Is Code A a good way ?
3: I needn't to add rememberSaveable for variable drawerOpen in #Composable fun myRoute(...) because view model will store data, right?
Code A
class MainViewModel : ViewModel() {
private val _drawerShouldBeOpened = MutableStateFlow(false)
val drawerShouldBeOpened: StateFlow<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen by MainViewModel.drawerShouldBeOpened.collectAsState() //Do I need add rememberSaveable ?
...
}
Code B
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = mutableStateOf(false)
val drawerShouldBeOpened: State<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = MainViewModel.drawerShouldBeOpened //Do I need add rememberSaveable ?
...
}
Code C
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = false
val drawerShouldBeOpened: Boolean = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = rememberSaveable { mutableStateOf(MainViewModel.drawerShouldBeOpened)) //Can I remove rememberSaveable ?
}
There are multiple questions here.
Let me answer whatever is possible.
1. Where should you use remember / rememberSaveable? (Code A, B, or C)
Only in code C it is required.
(No issues in using in code A and B as well, but no advantages there)
Reason,
In code A and B - the state is maintained in the view model. Hence the value survives recomposition.
But in code C, the state is created and maintained inside the composable. Hence remember is required for the value to survive recomposition.
More details in Docs
2. Why Code C is not used much?
Composable recomposition happens whenever there is a change in state, not the value.
Given below is a simple example to demonstrate the same.
class ToggleViewModel : ViewModel() {
private val _enabledStateFlow = MutableStateFlow(false)
val enabledStateFlow: StateFlow<Boolean> = _enabledStateFlow
private val _enabledState = mutableStateOf(false)
val enabledState: State<Boolean> = _enabledState
private var _enabled = false
val enabled: Boolean = _enabled
fun setEnabledStateFlow(isEnabled: Boolean) {
_enabledStateFlow.value = isEnabled
}
fun setEnabledState(isEnabled: Boolean) {
_enabledState.value = isEnabled
}
fun setEnabled(isEnabled: Boolean) {
_enabled = isEnabled
}
}
#Composable
fun BooleanToggle(
viewmodel: ToggleViewModel = ToggleViewModel(),
) {
val enabledStateFlow by viewmodel.enabledStateFlow.collectAsState()
val enabledState by viewmodel.enabledState
val enabled by rememberSaveable {
mutableStateOf(viewmodel.enabled)
}
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledStateFlow) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledStateFlow(!enabledStateFlow) }) {
Text("Toggle State Flow")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledState) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledState(!enabledState) }) {
Text("Toggle State")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabled) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabled(!enabled) }) {
Text("Toggle Value")
}
}
}
}
You can see that the third text will NOT update on clicking the button.
The reason is that the mutable state inside the composable was created using an initial value from the view model data. But further updates to that data will not be reflected in the composable.
To get updates, we have to use reactive data like Flow, LiveData, State, and their variants.
3. Using StateFlow vs State.
From the docs, you can see that compose supports Flow, LiveData and RxJava.
You can see in the usage that we are using collectAsState() for StateFlow.
The method converts StateFlow to State. So both can be used.
Use Flow if the layers beyond ViewModel (like repo) are the data sources and they use Flow data type.
Else MutableState should be fine.

Compose navigation title is not updating

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks

Select all text of TextField in Jetpack Compose

I'm using TextField component in Jetpack Compose.
How to select all text when it receive focus?
In this case you should use TextFieldValue as state of your TextField, and when it receive focus, you set the selection using the TextFieldValue state.
val state = remember {
mutableStateOf(TextFieldValue(""))
}
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
)
Here's the result:
Notice that depending on you're touching the cursor goes to the touched position instead of select the entire text. You can try to figure it out if this is a bug or a feature :)
#nglauber solution doesn't seems to work anymore.
Debugging shows that onFocusChanged is called before onValueChange and within one view life cycle. A selection changed during onFocusChanged has no effect on TextField, since it is overridden during onValueChange.
Here's a possible workaround:
var state by remember {
mutableStateOf(TextFieldValue("1231"))
}
var keepWholeSelection by remember { mutableStateOf(false) }
if (keepWholeSelection) {
// in case onValueChange was not called immediately after onFocusChanged
// the selection will be transferred correctly, so we don't need to redefine it anymore
SideEffect {
keepWholeSelection = false
}
}
TextField(
value = state,
onValueChange = { newState ->
if (keepWholeSelection) {
keepWholeSelection = false
state = newState.copy(
selection = TextRange(0, newState.text.length)
)
} else {
state = newState
}
},
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.text
state = state.copy(
selection = TextRange(0, text.length)
)
keepWholeSelection = true
}
}
)
I think it should be possible to make it easier, so I created this question on Compose issue tracker.
I didn't have 100% success with #nglauber answer. You should add a small delay and it works great. For example:
val state = remember {
mutableStateOf(TextFieldValue(""))
}
// get coroutine scope from composable
val scope = rememberCoroutineScope()
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged {
if (it.hasFocus) {
// start coroutine
scope.launch {
// add your preferred delay
delay(10)
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
}
)
I wrote a Modifier extension function that works in spite of bug pointed out by #Pylyp
fun Modifier.onFocusSelectAll(textFieldValueState: MutableState<TextFieldValue>): Modifier =
composed(
inspectorInfo = debugInspectorInfo {
name = "textFieldValueState"
properties["textFieldValueState"] = textFieldValueState
}
) {
var triggerEffect by remember {
mutableStateOf<Boolean?>(null)
}
if (triggerEffect != null) {
LaunchedEffect(triggerEffect) {
val tfv = textFieldValueState.value
textFieldValueState.value = tfv.copy(selection = TextRange(0, tfv.text.length))
}
}
Modifier.onFocusChanged { focusState ->
if (focusState.isFocused) {
triggerEffect = triggerEffect?.let { bool ->
!bool
} ?: true
}
}
}
usage
#Composable
fun SelectAllOnFocusDemo() {
var tfvState = remember {
mutableStateOf(TextFieldValue("initial text"))
}
TextField(
modifier = Modifier.onFocusSelectAll(tfvState),
value = tfvState.value,
onValueChange = { tfvState.value = it },
)
}
I want to add to the Phil's answer I wanted to update the state dynamically and I ended up with this:
var state by remember(textVal) {
mutableStateOf(TextFieldValue(text = textVal, selection = TextRange(textVal.length)))
}
It does two things, first it updates the field if your textVal changes, also puts the cursor at the end.