In a Composable function, I can pass as parameter the State, or the value of the State. Any reason for preferring to pass the value of the State, instead of the State?
In both cases, the composable is stateless, so why should I distinguish both cases?
It's possible to pass state's value. For example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isLoading = mutableStateOf(false)
val onClickAtButton = {
lifecycleScope.launch(Dispatchers.Main) {
isLoading.value = true
withContext(Dispatchers.IO) {
//Do some heavy operation live REST call
}
isLoading.value = false
}
}
setContent {
MyComposable(isLoading.value, onClickAtButton)
}
}
}
#Composable
fun MyComposable(
isLoading: Boolean = false,
onClickAtButton: () -> Unit = {}
){
Box(modifier = Modifier.fillMaxSize(){
Button(onClick = onClickAtButton)
if(isLoading){
CircularProgressIndicator()
}
}
}
Hope it helps somebody.
There is a slight difference between passing State or just the value of a State regarding recomposition.
Let's start with passing State:
#Composable
fun Example1(text: State<String>) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: State<String>) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text.value)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
On first start you will see the log output
Example1 recomposition
Example2 recomposition
However when you click the button, you will only see an additional
Example2 recomposition
Because you're passing down State and only Example2 is reading the state, Example1 does not need to be recomposed.
Let's change the parameters to a plain type:
#Composable
fun Example1(text: String) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: String) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text.value)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
When you click the button now, you will see two additional lines in the log output
Example1 recomposition
Example2 recomposition
Since text is now a plain type of the function signatures of both composables, both need to be recomposed when the value changes.
However always passing down State can become quite cumbersome. Compose is quite good at detecting what needs to be recomposed so this should be considered a micro optimization. I just wanted to point out that there is a slight difference which every developer using Compose should know about.
Related
So I have the following composable function:
#Composable
fun SearchResult() {
if (searchInput.isNotEmpty()) {
Column() {
Text("Search Result!")
}
}
}
Then I called the function from here:
private fun updateContent() {
setContent {
ChemistryAssistantTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column() {
Title(" Chemistry Assistant ", " Made by Saket Tamrakar ")
Column() {
SearchElements()
SearchResult() // Here
//Options()
}
}
}
}
}
}
The issue here is that the function gets correctly called in the beginning, when I invoke updateContent() here:
OutlinedTextField(value = input, placeholder = { Text("Search for any element!") }, onValueChange = {
input = it
searchInput = it.text
updateContent()
})
Control does reach the function (at least according to what the debugger tells me), but still fails to execute the function body.
Any ideas?
You should keep searchInput as a state like:
val searchInput by mutableStateOf("")
This ensures that whenever the value of searchInput changes, any composable whose structure depends on it will also recompose(i.e recall the function).
Hope this solves your issue.
Apparently moving the variable searchInput:
#Composable
fun SearchResult() {
if (/*this one*/searchInput.isNotEmpty()) {
Column() {
Text("Search Result!")
}
}
}
..inside the MainActivity class fixed the issue.
I am trying to implement Navigation using single activity and
multiple Composable Screens.
This is my NavHost:
#Composable
#ExperimentalFoundationApi
fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = HOME.route,
viewModelProvider: ViewModelProvider,
speech: SpeechHelper
) = NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(route = HOME.route) {
with(viewModelProvider[HomeViewModel::class.java]) {
HomeScreen(
speech = speech,
viewModel = this,
modifier = Modifier.onKeyEvent { handleKeyEvent(it, this) }
) {
navController.navigateTo(it)
}
}
}
composable(route = Destination.VOLUME_SETTINGS.route) {
VolumeSettingsScreen(
viewModelProvider[VolumeSettingsViewModel::class.java]
) { navController.navigateUp() }
}
}
fun NavHostController.navigateTo(
navigateRoute: String,
willGoBackTo: String = HOME.route
): Unit = navigate(navigateRoute) {
popUpTo(willGoBackTo) { inclusive = true }
}
My screen looks like this:
#Composable
fun HomeScreen(
speech: SpeechHelper,
viewModel: HomeViewModel,
modifier: Modifier,
onNavigationRequested: (String) -> Unit
) {
MyBlindAssistantTheme {
val requester = remember { FocusRequester() }
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak(
R.string.welcome_
.withStrResPlaceholder(R.string.text_home_screen)
.toSpeechUiModel()
)
)
uiState?.let {
when (it) {
is UiState.Speak -> speech.speak(it.speechUiModel)
is UiState.SpeakRes -> speech.speak(it.speechResUiModel.speechUiModel())
is UiState.Navigate -> onNavigationRequested(it.route)
}
}
Column(
modifier
.focusRequester(requester)
.focusable(true)
.fillMaxSize()
) {
val rowModifier = Modifier.weight(1f)
Row(rowModifier) {...}
}
LaunchedEffect(Unit) {
requester.requestFocus()
}
}
}
This is the ViewModel:
class HomeViewModel : ViewModel() {
private val mutableUiState: MutableStateFlow<UiState?> = MutableStateFlow(null)
val uiState = mutableUiState.asStateFlow()
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(Destination.VOLUME_SETTINGS.route.toNavigationState())
}
}
When a button is clicked the ViewModel is called and the NavigateUiState is emitted... but it keeps being emitted after the next screen is loaded and that causes infinite screen reloading. What should be done to avoid this?
I re-implemented your posted code with 2 screens, HomeScreen and SettingScreen and stripped out some part of the UiState class and its usages.
The issue is in your HomeScreen composable, not in the StateFlow emission.
You have this mutableState
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak
)
that is being observed in one of your when block that executes a navigation callback.
uiState?.let {
when (it) {
is UiState.Navigate -> {
onNavigationRequested(it.route)
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
When your ViewModel function is called
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(UiState.Navigate(Destination.SETTINGS_SCREEN.route))
}
it will update uiState, setting its value to Navigate, observed by HomeScreen, satisfies the when block and then triggers the callback to navigate to the next screen.
Now based on the official Docs,
You should only call navigate() as part of a callback and not as part
of your composable itself, to avoid calling navigate() on every
recomposition.
but in your case, the navigation is triggered by an observed mutableState, and the mutableState is part of your HomeScreen composable.
It seems like when the navController performs a navigation and the NavHost being a Composable
#Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) { ... }
it will execute a re-composition, because of it, it will call again the HomeScreen (HomeScreen is not re-composed, its state remains the same) and because the HomeScreen's UiState value is still set to Navigate, it satisfies the when block, triggers again the callback to navigate, and NavHost re-composes, an infinite cycle is then created.
What I did (and its very ugly) is I created a boolean flag inside the viewModel, used it to wrap the callback conditionally,
uiState?.let {
when (it) {
is UiState.Navigate -> {
if (!viewModel.navigated) {
onNavigationRequested(it.route)
viewModel.navigated = true
} else {
// dirty empty else
}
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
}
and setting it to true afterwards, preventing the cycle.
I can hardly guess your compose implementation structure but I usually don't mix my one-time event actions and UiState, instead I have a separate UiEvent sealed class that will group "one-time" events such as the following:
Snackbar
Toast
Navigation
and having them emitted as a SharedFlow emissions because these events doesn't need any initial state or initial value.
Continuing further, I created this class
sealed class UiEvent {
data class Navigate(val route: String) : UiEvent()
}
use it in the ViewModel as a type (Navigate in this case),
private val _event : MutableSharedFlow<UiEvent> = MutableSharedFlow()
val event = _event.asSharedFlow()
fun onNavigateButtonClicked(){
viewModelScope.launch {
_event.emit(UiEvent.Navigate(Destination.SETTINGS_SCREEN.route))
}
}
and observe it in HomeScreen this way via LaunchedEffect, triggering the navigation in it without the callback being bound to any observed state.
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
onNavigationRequested(it.route)
}
}
}
}
This approach doesn't introduce the infinite navigation cycle and the dirty boolean checking is not needed anymore.
Also have a look this S.O post, similar to your case
I've been checking out the Performance best practices for Jetpack Compose Google I/O, in there it's stated that this code should only re-execute the Text() function, since only this function reads a value that changes.
private class NameHolder(var name: String)
#Composable
private fun LittleText(nameHolder: NameHolder) {
Box {
Text(text = "Nombre: ${nameHolder.name}")
println("compose 2")
}
println("compose 1")
}
however when I run it I can see that for every change both prints execute as well.
I also tested with something like this:
#Composable
private fun LittleText(name: String) {
Box {
Text(text = "Nombre: $name")
println("compose 2")
}
println("compose 1")
}
With the same result, I'm changing the text with a TextField, like this:
var name by remember { mutableStateOf("name") }
TextField(
value = name,
onValueChange = {
name = it
}
)
LittleText(name)
What I'm I doing wrong? How can I achieve this behaviour and have only the Text re-executing the composition?
I found an answer that cover this:
#Composable
fun TestingCompose() {
Column {
TestView()
println("compose 1")
}
}
#Composable
fun TestView() {
val textFieldValue = remember { mutableStateOf(TextFieldValue("")) }
TextField(textFieldValue)
println("compose 2")
}
#Composable
fun TextField(textFieldValue: MutableState<TextFieldValue>) {
TextField(
value = textFieldValue.value,
onValueChange = { textFieldValue.value = it }
)
println("compose 3")
}
I'm still trying to fully understand it, so any insight would be greatly appreciated, but checking the log while testing this shows that only the composable containing the TextField gets re-executed with every character.
We have the below compose which works well.
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
But if we have the timerStartStop passed through the function parameter, I cannot do as below
#Composable
fun MyInnerControl(timerStartStop: Boolean) {
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I can pass as below, but then I have to change my timerStartStop to timerStartStop.value
#Composable
fun MyInnerControl(timerStartStop: MutableState<Boolean>) {
Button(onClick = {
timerStartStop.value = !timerStartStop.value
}) {
Text(if (timerStartStop.value) "Stop" else "Start")
}
}
Is there any way to have the MutableState<Boolean> passed over the argument, yet within the function we can use the delegate approach, i.e. using timerStartStop instead of timerStartStop.value?
Jetpack Compose tells you to respect Single Source of Truth principle, which means that you can not pass state as argument if you want to change that state inside your function.
So generally you have to choose one of two approaches:
Pass both timerStartStop variable and onClick callback
#Composable
fun MyInnerControl(timerStartStop: Boolean, onClick: ()->Unit) {
Button(onClick = onClick) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
Pass none of these
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I think it's better to apply proper state hoisting methods and give MyInnerControl the ability to both read and edit the parameter in the following way.
#Composable
fun ParentComposable() {
val isTimerRunning by remember { mutableStateOf(false) } }
MyInnerControl(
isTimerRunning = isTimerRunning,
flipTimerState {
isTimerRunning = !isTimerRunning
}
}
}
#Composable
fun MyInnerControl(
isTimerRunning: Boolean,
flipTimerState: () -> Unit,
) {
Button(
onClick = { flipTimerState() }
) {
Text(if (isTimerRunning) "Stop" else "Start")
}
}
I'm trying to pass the MutableState variable over another function. The below is all good. But I don't like the myMutableState.value
#Composable
fun Func() {
val myMutableState = remember { mutableStateOf(true) }
Column {
AnotherFunc(mutableValue = myMutableState)
Text(if (myMutableState.value) "Stop" else "Start")
}
}
#Composable
fun AnotherFunc(mutableValue: MutableState<Boolean>) {
}
So I decided to use val myMutableState by remember { mutableStateOf(true) }, as shown below. I no longer need to use myMutableState.value as shown below.
Unfortunately, the below won't compile. This is because I cannot pass it over the function AnotherFunc(mutableValue = myMutableState)
#Composable
fun Func() {
val myMutableState by remember { mutableStateOf(true) }
Column {
AnotherFunc(mutableValue = myMutableState) // Error here
Text(if (myMutableState) "Stop" else "Start")
}
}
#Composable
fun AnotherFunc(mutableValue: MutableState<Boolean>) {
}
How can I still use by and still able to pass the MutableState over the function?
=-0987our composable function should just take a boolean:
#Composable
fun AnotherFunc(mutableValue: Boolean) {
}
Not sure why your composable function (AnotherFun) needs to have a mutable state. The calling function (Fun) will automatically recompose when the value changes, triggering recomposition of AnotherFun.