Jetpack Compose: mutableStateOf doesn't update with flow - android

I have a ViewModel which uses Flow to get a Note object from my Room database:
var uiState by mutableStateOf(NoteUiState())
private set
private fun getNoteById(argument: Int) {
viewModelScope.launch {
try {
repository.getNoteById(argument).collect { note ->
uiState = NoteUiState(note = note)
}
} catch (e: Exception) {
uiState = NoteUiState(error = true)
}
}
}
Note class:
#Entity(tableName = "notes")
data class Note(
#PrimaryKey(autoGenerate = true) val id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "text") val text: String = "",
) {
override fun toString() = title
}
This approach works fine, until I try to make a mutable strings with the values of the Note object as their default so I can update 2 TextField composables:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Note(
note: DataNote,
isNewNote: Boolean,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit,
trued: String,
) {
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.text) }
TextField(
value = title,
onValueChange = { title = it },
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Title") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
TextField(
value = content,
onValueChange = { content = it },
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Content") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
}
For some reason the first time the Note object is called it's null, so I want a way to update the title and content variables.
The Note object itself updates without issue, however the title and content variables never change from the initial value. How can I update the title and content variables while also making them work for the textfield?

I found out how to make the Textfield work while also getting the inital value from the object. The issue was that the Note object was called as null on the first call, so the mutableStateFlow didnt get the initial values.
First, I had to pass the actual state as a MutableStateFlow to my composable:
#Composable
private fun Note(
state: MutableStateFlow<NoteUiState>,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit
) {
...
Next, I just had to get the Note object by calling collectAsState():
val currentNote = state.collectAsState().value.note
Finally, all that was needed was to pass the currentNote object text and title in the value of the Textfield, and on onValueChange to update the state object itself via a copy:
This is the complete solution:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Note(
state: MutableStateFlow<NoteUiState>,
createNote: (DataNote) -> Unit,
updateNote: (DataNote) -> Unit,
back: () -> Unit
) {
val currentNote = state.collectAsState().value.note
Column(Modifier.fillMaxSize()) {
TextField(
value = currentNote.title,
onValueChange = {
state.value = state.value.copy(note = currentNote.copy(title = it))
},
modifier = Modifier
.fillMaxWidth(),
placeholder = { Text(text = "Title") },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
)
}
}
I'm not sure is this is a clean solution, but is the only way it worked for me, thanks for the feedback, opinions on this approach are always welcomed.

You should think about state hoisting and about having a single source of truth.
Really you need to define where your state will live, if on the viewmodel or on the composable functions.If you are only going to use the state (your note) on the ui then it's ok to hoist your state up to the Note composable function.
But if you need that state in something like another repo, insert it somewhere else or in general to do operations with it, the probably you should hoist it up to the viewmodel (you already have it there).
So use the property of your viewmodel directly in your composable and add a function in your viewmodel to mutate the state and pass this function to the onValueChanged lambda.

var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.text) }
remember block executes only on 1st composition and then value will remembered until decomposition or u need to change it externally through '=' assignment operator,
.
instated of this
TextField(
value = title)
write this way
TextField(
value = note.title)

Related

onValueChange of BasicTextField is not triggered on setting value to TextFieldValue("") in Jetpack Compose

I want to execute some code when the value of BasicTextfield changes in Jetpack Compose.
Everything works fine in 2 conditions:
for any value change.
if all the textfield value is cleared using the device keyboard
But,
When I try to change the state value to empty text on click of a button, using this code :
textfieldstate.value = TextFIeldValue("")
onValueChange is not triggered.
Although if I set it to any other value, onValueChange is triggered.
textfieldstate.value = TextFIeldValue("FOO")
Code of Button/Icon click:
Icon(modifier = Modifier.clickable {
textfieldstate.value = TextFieldValue("")
}) {.....}
Is there a way to trigger onValueChange of BasicTextField when value of the field is cleared from an external button click event??
If you want to do it all at once as is more recommended, I would do this:
#Composable
fun AppContent(
viewModel: MyViewModel
) {
val state by viewModel.uiState.collectAsState()
MyPanel(
state = MyViewModel,
onValueChange = viewModel::onValueChange,
onClickButton = viewModel::onClickButton
)
}
#Composable
fun MyPanel(
state: MyTextFieldState,
onValueChange: (String) -> Unit,
onClickButton: () -> Unit
) {
TextField(
value = state.text,
onValueChange = onValueChange(it)
)
Button(
onClick = { onClickButton() }
) {
...
}
}
class MyViewModel: ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState = _uiState.asStateFlow()
fun onValueChange(str: String) {
_uiState.value = _uiState.value.copy(text = str)
}
fun onClickButton() {
_uiState.value = _uiState.value.copy(text = "")
}
}
data class MyUiState(
val text: String = ""
)
The code above mainly elevates the state of the TextField, processes all things in the viewModel, and wraps a layer of UI state with a data class. If there are other requirements, you can also add different parameters, for example, if there is an error in the TextField, it can be written as:
data class MyUiState(
val text: String = "",
val isTextError: Boolean = false
)
The onValueChange callback is useful to be informed about the latest state of the text input by users.
If you want to trigger some action when the state of (textFieldValue) changes, you can use a side effect like LaunchedEffect.
Something like:
var textFieldValue by remember() {
mutableStateOf(TextFieldValue("test" ))
}
LaunchedEffect(textFieldValue) {
//doSomething()
}
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
}
)
OutlinedButton(
onClick = { textFieldValue = textFieldValue.copy("") }
) {
Text(text = "Button")
}

Can I wrap Modifier with remember in Jetpack Compose?

In order to share settings among of compose functions, I create a class AboutState() and a compose fun rememberAboutState() to persist settings.
I don't know if I can wrap Modifier with remember in the solution.
The Code A can work well, but I don't know if it maybe cause problem when I wrap Modifier with remember, I think Modifier is special class and it's polymorphic based invoked.
Code A
#Composable
fun ScreenAbout(
aboutState: AboutState = rememberAboutState()
) {
Column() {
Hello(aboutState)
World(aboutState)
}
}
#Composable
fun Hello(
aboutState: AboutState
) {
Text("Hello",aboutState.modifier)
}
#Composable
fun World(
aboutState: AboutState
) {
Text("World",aboutState.modifier)
}
class AboutState(
val textStyle: TextStyle,
val modifier: Modifier=Modifier
) {
val rowSpace: Dp = 20.dp
}
#Composable
fun rememberAboutState(): AboutState {
val aboutState = AboutState(
textStyle = MaterialTheme.typography.body1.copy(
color=Color.Red
),
modifier=Modifier.padding(start = 80.dp)
)
return remember {
aboutState
}
}
There wouldn't be a problem passing a Modifier to a class. What you actually defined above, even if named State, is not class that acts as a State, it would me more appropriate name it as HelloStyle, HelloDefaults.style(), etc.
It would be more appropriate to name a class XState when it should have internal or public MutableState that can trigger recomposition or you can get current State of Composable or Modifier due to changes. It shouldn't contain only styling but state mechanism either to change or observe state of the Composble such as ScrollState or PagerState.
When you have a State wrapper object common way of having a stateful Modifier or Modifier with memory or Modifiers with Compose scope is using Modifier.composed{} and passing State to Modifier, not the other way around.
When do you need Modifier.composed { ... }?
fun Modifier.composedModifier(aboutState: AboutState) = composed(
factory = {
val color = remember { getRandomColor() }
aboutState.color = color
Modifier.background(aboutState.color)
}
)
In this example even if it's not practical getRandomColor is created once in recomposition and same color is used.
A zoom modifier i use for zooming in this library is as
fun Modifier.zoom(
key: Any? = Unit,
consume: Boolean = true,
clip: Boolean = true,
zoomState: ZoomState,
onGestureStart: ((ZoomData) -> Unit)? = null,
onGesture: ((ZoomData) -> Unit)? = null,
onGestureEnd: ((ZoomData) -> Unit)? = null
) = composed(
factory = {
val coroutineScope = rememberCoroutineScope()
// Current Zoom level
var zoomLevel by remember { mutableStateOf(ZoomLevel.Min) }
// Rest of the code
},
inspectorInfo = {
name = "zoom"
properties["key"] = key
properties["clip"] = clip
properties["consume"] = consume
properties["zoomState"] = zoomState
properties["onGestureStart"] = onGestureStart
properties["onGesture"] = onGesture
properties["onGestureEnd"] = onGestureEnd
}
)
Another practical example for this is Modifier.scroll that uses rememberCoroutineScope(), you can also remember object too to not intantiate another object in recomposition
#OptIn(ExperimentalFoundationApi::class)
private fun Modifier.scroll(
state: ScrollState,
reverseScrolling: Boolean,
flingBehavior: FlingBehavior?,
isScrollable: Boolean,
isVertical: Boolean
) = composed(
factory = {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
val coroutineScope = rememberCoroutineScope()
// Rest of the code
},
inspectorInfo = debugInspectorInfo {
name = "scroll"
properties["state"] = state
properties["reverseScrolling"] = reverseScrolling
properties["flingBehavior"] = flingBehavior
properties["isScrollable"] = isScrollable
properties["isVertical"] = isVertical
}
)

How to assign the value of a textfield to a hoistedState in compose?

I want to make a reusable component including between other things a TextField, like
#Composable
fun ReusableFormField(textFieldValue: TextFieldValue){
--label stuff here--
TextField(value = textFieldValue, onValueChange = {
textFieldValue = it)
})
--helper stuff here
}
But I can't find a way to assign the textFieldValue to a wrapping component as you cannot pass a variable by reference with something like ref or out in the parameters.
You need to provide a lambda parameter to ReusableFormField to update the value.
#Composable
fun ReusableFormField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit
){
// --label stuff here--
TextField(value = value, onValueChange = onValueChange)
// --helper stuff here
}
If you remembered textFieldValue state above you can pass event top and modify state there. then it will be recomposed.
#Composable
fun ParentComposable(){
var textFieldValue by remember {
mutableStateOf(TextFieldValue(""))
}
ReusableFormField(
textFieldValue,
){
textFieldValue = it
}
}
#Composable
fun ReusableFormField(
textFieldValue: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit
){
TextField(value = textFieldValue, onValueChange = onValueChange)
}

Jetpack Compose - pass an object through composable callback

In this app, I have a screen where you can enter a title and content for a Note.
The screen has two composables DetailScreen() and DetailScreenContent.
Detailscreen has the scaffold and appbars and calls DetailScreenContents() which has two TextFields and a button.
I'm expecting the user to enter text in these fields and then press the button which will package the text into a NOTE object. My question is, how to pass the NOTE to the upper composable which is DETAILSCREEN() with a callback like=
onclick: -> Note or any other efficient way?
#Composable
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent()
}
}
#Composable
fun DetailScreenContent() {
val titleValue = remember { mutableStateOf("")}
val contentValue = remember { mutableStateOf("")}
val endnote by remember{ mutableStateOf(Note(
Title = titleValue.value,
Content = contentValue.value))}
Column(modifier = Modifier.fillMaxSize()) {
OutlinedTextField(value = titleValue.value,
onValueChange = {titleValue.value = it},
singleLine = true,
label = {Text("")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp),
shape = cardShapes.small
)
OutlinedTextField(value = contentValue.value, onValueChange = {
contentValue.value = it
},
label = {Text("Content")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp, top = 3.dp)
.height(200.dp),
shape = cardShapes.small,
)
Row(horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()){
Button(onClick = {
/**return the object to the upper composable**/
}, shape = cardShapes.small) {
Text(text = stringResource(R.string.Finish))
}
}
}
You could use state hoisting. Using lambdas is the most common way of hoisting state here.
Ok so here's DetailScreenContent(), say
fun DetailScreenContent(
processNote: (Note) -> Unit
){
Button( onClick = { processNote(/*Object to be "returned"*/) }
}
We are not literally returning anything, but we are hoisting the state up the hierarchy. Now, in DetailsScreen
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent(
processNote = {note -> //This is the passed object
/*Perform operations*/
}
)
//You could also extract the processNote as a variable, like so
/*
val processNote = (Note) {
Reference the note as "it" here
}
*/
}
}
This assumes that there is a type Note (something like a data class or so, the object of which type is being passed up, get it?)
That's how we hoist our state and hoist it up to the viewmodel. Remember, compose renders state based on variables here, making it crucial to preserve the variables, making sure they are not modified willy nilly and read from random places. There should be, at a time, only one instance of the variables, which should be modified as and when necessary, and should be read from a common place. This is where viewmodels are helpful. You store all the variables (state) inside the viewmodel, and hoist the reads and modifications to there. It must act as a single source of truth for the app.

How to trigger recomposition when modify the parent data using CompositionLocal

when I use CompositionLocal, I have got the data from the parent and modify it, but I found it would not trigger the child recomposition.
I have successfully change the data, which can be proved through that when I add an extra state in the child composable then change it to trigger recomposition I can get the new data.
Is anybody can give me help?
Append
code like below
data class GlobalState(var count: Int = 0)
val LocalAppState = compositionLocalOf { GlobalState() }
#Composable
fun App() {
CompositionLocalProvider(LocalAppState provides GlobalState()) {
CountPage(globalState = LocalAppState.current)
}
}
#Composable
fun CountPage(globalState: GlobalState) {
// use it request recomposition worked
// val recomposeScope = currentRecomposeScope
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.clickable {
globalState.count++
// recomposeScope.invalidate()
}) {
Text("count ${globalState.count}")
}
}
I found a workaround is using currentRecomposable to force recomposition, maybe there is a better way and pls tell me.
The composition local is a red herring here. Since GlobalScope is not observable composition is not notified that it changed. The easiest change is to modify the definition of GlobalState to,
class GlobalState(count: Int) {
var count by mutableStateOf(count)
}
This will automatically notify compose that the value of count has changed.
I am not sure why you are using compositionLocalOf in this way.
Using the State hoisting pattern you can use two parameters in to the composable:
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:
data class GlobalState(var count: Int = 0)
#Composable
fun App() {
var counter by remember { mutableStateOf(GlobalState(0)) }
CountPage(
globalState = counter,
onUpdateCount = {
counter = counter.copy(count = counter.count +1)
}
)
}
#Composable
fun CountPage(globalState: GlobalState, onUpdateCount: () -> Unit) {
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.clickable (
onClick = onUpdateCount
)) {
Text("count ${globalState.count}")
}
}
You can declare your data as a MutableState and either provide separately the getter and the setter or just provide the MutableState object directly.
internal val LocalTest = compositionLocalOf<Boolean> { error("lalalalalala") }
internal val LocalSetTest = compositionLocalOf<(Boolean) -> Unit> { error("lalalalalala") }
#Composable
fun TestProvider(content: #Composable() () -> Unit) {
val (test, setTest) = remember { mutableStateOf(false) }
CompositionLocalProvider(
LocalTest provides test,
LocalSetTest provides setTest,
) {
content()
}
}
Inside a child component you can do:
#Composable
fun Child() {
val test = LocalTest.current
val setTest = LocalSetTest.current
Column {
Button(onClick = { setTest(!test) }) {
Text(test.toString())
}
}
}

Categories

Resources