How to update composable state from outside? - android

For example it working with state{} within composable function.
#Composable
fun CounterButton(text: String) {
val count = state { 0 }
Button(
modifier = Modifier.padding(8.dp),
onClick = { count.value++ }) {
Text(text = "$text ${count.value}")
}
}
But how to update value outside of composable function?
I found a mutableStateOf() function and MutableState interface which can be created outside composable function, but no effect after value update.
Not working example of my attempts:
private val timerState = mutableStateOf("no value", StructurallyEqual)
#Composable
private fun TimerUi() {
Button(onClick = presenter::onTimerButtonClick) {
Text(text = "Timer Button")
}
Text(text = timerState.value)
}
override fun updateTime(time: String) {
timerState.value = time
}
Compose version is 0.1.0-dev14

You can do it by exposing the state through a parameter of the controlled composable function and instantiate it externally from the controlling composable.
Instead of:
#Composable
fun CounterButton(text: String) {
val count = remember { mutableStateOf(0) }
//....
}
You can do something like:
#Composable
fun screen() {
val counter = remember { mutableStateOf(0) }
Column {
CounterButton(
text = "Click count:",
count = counter.value,
updateCount = { newCount ->
counter.value = newCount
}
)
}
}
#Composable
fun CounterButton(text: String, count: Int, updateCount: (Int) -> Unit) {
Button(onClick = { updateCount(count+1) }) {
Text(text = "$text ${count}")
}
}
In this way you can make internal state controllable by the function that called it.
It is called state hoisting.

Related

How to trigger JetPack recomposition when state is re-assigned it's current value

The below code works as desired: the canvas gets recomposed each time the user either clicks the canvas itself or clicks the topBar icon, no matter how many times or in what order. In addition, the state variable value reveals something I want to know: where the user clicked. (Values 0 and 1 mean the icon was clicked and values 2 and 3 mean the canvas).
However, if the canvasState and iconState variables are set to their respective V1 functions instead of the V2 functions, then clicking the canvas or icon multiple times in a row is not detected. Apparently this is because the V1 functions can re-assign the same value to the state variable, unlike the V2 functions.
Since I'm using the neverEqualPolicy(), I thought I didn't have to assign a different value to the state variable to trigger a recompose. As a noob to Kotlin and Compose, what am I misunderstanding?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
#Composable
fun MyApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
val canvasStateV1 = { state = 0 }
val iconStateV1 = { state = 2 }
val canvasStateV2 = { state = if (state == 0) { 1 } else { 0 } }
val iconStateV2 = { state = if (state == 2) { 3 } else { 2 } }
val iconState = iconStateV2
val canvasState = canvasStateV2
Scaffold(
topBar = { TopBar(canvasState) },
content = { padding ->
Column(Modifier.padding(padding)) {
Screen(state, iconState)
}
}
)
}
#Composable
fun TopBar(iconState: () -> Unit) {
TopAppBar(
title = { Text("This is a test") },
actions = {
IconButton(onClick = { iconState() }) {
Icon(Icons.Filled.AddCircle, null)
}
}
)
}
#Composable
fun Screen(state: Int, canvasState: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.aspectRatio(ratio = 1f)
.background(color = MaterialTheme.colors.onSurface)
.pointerInput(Unit) {
detectTapGestures(
onTap = { canvasState() },
)
}
) {
Canvas(
modifier = Modifier.fillMaxSize().clipToBounds()
) {
Log.d("Debug", "Canvas: state = $state")
}
}
}
}
I didn't know other things to try to get the neverEqualPolicy() to work as expected.
I think the main reason for this is because the function Screen() is skippable. If you add the state as a MutableState instead of the Int itself, you will see that the Log.d gets called each time the state value gets updated. Same goes for merging the Screen() function into Column in MyApp
Compose analyses each function during build time. The screen functions receives an integer value, this is an immutable value, so the function itself becomes skippable.
To analyse which function is skippable/stable (and which is not), you can run a report during the build phase
This repo shows how
EDIT:
In this example you have two buttons, one changes the value, one just sets the same value. When setting the same value, you only see the Log.d of the local recomposition. When changing the state value, you see two log lines. the local and external both go through the recomposition.
#Composable
fun StackOverflowApp() {
var state by remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state = state }) {
Text(text = "State same value")
}
Button(onClick = { state += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = $state")
Log.d("TAG","Recomposition local")
ExternalText(state)
}
}
/**
* A skippable function
*
* restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ExternalText(
stable state: Int
)
*/
#Composable
fun ExternalText(state: Int){
Text(text = "[external] current State = $state")
Log.d("TAG","Recomposition external")
}
You can also pass the MutableState instead of the int value itself, when you pass the mutableState, the neverEqualPolicy is still in play. Each interaction fires both log lines
#Composable
fun StackOverflowApp() {
var state = remember { mutableStateOf(value = 0, policy = neverEqualPolicy()) }
Column() {
Button(onClick = { state.value = state.value }) {
Text(text = "State same value")
}
Button(onClick = { state.value += 1 }) {
Text(text = "State up")
}
Text(text = "[local] current State = ${state.value}")
Log.d("TAG","Recomposition internal")
ExternalText(state)
}
}
#Composable
fun ExternalText(state: MutableState<Int>){
Text(text = "[external] current State = ${state.value}")
Log.d("TAG","Recomposition external")
}

Use of State hosting to change variable in jetpack compose

I want to change the value of variable in jetpack compose. I am trying to use Stateful and Stateless with some code, but it have some problem to increment the value. Can you guys guide me on this.
ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = { index },
onIndexChange = {
index = it
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: () -> Int,
onIndexChange: (Int) -> Unit,
) {
Button(onClick = { onIndexChange(index()++) }) {
Text(text = "Click Me $index")
}
}
I am getting error on index()++.
Using the general pattern for state hoisting your stateless composable should have two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
In your case:
index: Int,
onIndexChange: (Int) -> Unit
Also you should respect the Encapsulated properties: Only stateful composables can modify their state. It's completely internal.
Use onIndexChange(index+1) instead of onIndexChange(index()++). In this way the state is modified by the ItemColorStateful.
You can use:
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index = it
}
)
}
}
#Composable
fun ButtonScopeStateless(
index: Int, //value=the current value to display
onIndexChange: (Int) -> Unit //an event that requests the value to change, where Int is the proposed new value
) {
Button(onClick = { onIndexChange(index+1) }) {
Text(text = "Click Me $index")
}
}
ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index++
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: Int,
onIndexChange: () -> Unit,
) {
Button(onClick = {
onIndexChange()
}) {
Text(text = "Click Me $index")
}
}

How to save mutable state of TextField JetpackCompose

Description:
It's pretty basic problem but I've tried few solutions and nothing is working for me. I simply want to save state of BasicTextField in ViewModel. I tried mutableStateOf("SampleText") and text appears in BasicTextField but is not editable, even keyboard didn't appear.
ViewModel Code:
#HiltViewModel
class NewWorkoutViewModel #Inject constructor(...) : ViewModel() {
var workoutTitle by mutableStateOf("")
...
}
Screen Code:
#Composable
fun NewWorkoutScreen(
navController: NavController,
viewModel: NewWorkoutViewModel = hiltViewModel()
) {
Scaffold(...) { contentPadding ->
LazyColumn(...) {
item {
FillInContentBox(title = "Title:") {
// TextField which is not working
BasicTextField(textStyle = TextStyle(fontSize = 20.sp),
value = viewModel.workoutTitle,
onValueChange ={viewModel.workoutTitle = it})
}
}
}
}
}
#Composable
fun FillInContentBox(title: String = "", content: #Composable () -> Unit = {}) {
Box(...) {
Column(...) {
Text(text = title)
content()
}
}
}
Second attempt:
I tried also using State Flow but still, I can't edit (fill in) text into TextField.
ViewModel Code:
#HiltViewModel
class NewWorkoutViewModel #Inject constructor(...) : ViewModel() {
private val _workoutTitle = MutableStateFlow("")
var workoutTitle = _workoutTitle.asStateFlow()
fun setWorkoutTitle(workoutTitle: String){
_workoutTitle.value = workoutTitle
}
...
}
Screen Code:
#Composable
fun NewWorkoutScreen(
navController: NavController,
viewModel: NewWorkoutViewModel = hiltViewModel()
) {
Scaffold(...) { contentPadding ->
LazyColumn(...) {
item {
FillInContentBox(title = "Title:") {
// TextField which is not working
BasicTextField(textStyle = TextStyle(fontSize = 20.sp),
value = viewModel.workoutTitle.collectAsState().value,
onValueChange = viewModel::setWorkoutTitle)
}
}
}
}
}
#Composable
fun FillInContentBox(title: String = "", content: #Composable () -> Unit = {}) {
Box(...) {
Column(...) {
Text(text = title)
content()
}
}
}
You can do it this way:
Create variable inside your composable
var workoutTitle = remember{mutableStateOf("")}
Pass your variable value in your textfield:
TextField(
value = workoutTitle.value,
onValueChange = {
/* You can store the value of your text view inside your
view model maybe as livedata object or state anything which suits you
*/
}
)

How to pass events to Composables?

I have two composables like this:
#Composable
fun Composable1(viewModel: MyViewModel) {
LaunchedEffect(Unit) {
viewModel.eventsFlow.collect { event ->
if(event is ShowSnackbar) {
// Send this event to Composable2 to show snackbar
}
}
}
Composable2(...) // passing some data and lambdas
}
#Composable
fun Composable2(...) {
val scaffoldState = rememberScaffoldState()
// On receiving event, show a snackbar
Scaffold(scaffoldState) {
// Other stuff
}
}
(If another ShowSnackbar event comes while one snackbar is visible, I want to ignore that new event)
How to send such an event from one composable to another?
I created a small example. I hope It is help you. In my case I generate a "event" by means of clicking a button
class ComposeActivity5 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutorialTheme {
Composable1()
}
}
}
}
#Composable
fun Composable1() {
val scaffoldState = rememberScaffoldState()
var showHide by remember { mutableStateOf(false) }
var pressCount by remember { mutableStateOf(0) }
Scaffold(
scaffoldState = scaffoldState,
content = { innerPadding ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
Button(onClick = {
pressCount++
showHide = true
}) {
Text(text = "Test")
}
Composable2(scaffoldState, showHide, pressCount) {
showHide = false
}
}
}
)
}
#Composable
fun Composable2(
scaffoldState: ScaffoldState,
showHide: Boolean,
pressCount: Int,
onDismiss: () -> Unit
) {
val mostRecentOnDismiss by rememberUpdatedState(onDismiss)
LaunchedEffect(scaffoldState, showHide) {
if (showHide) {
scaffoldState.snackbarHostState.showSnackbar(
message = "We are ignore press button to show Snackbar. Total number of clicks $pressCount",
actionLabel = "Close",
duration = SnackbarDuration.Short,
)
mostRecentOnDismiss()
}
}
}

How to use by delegate for mutableState yet make it passable to another function?

I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}

Categories

Resources