Can we create out own custom rememberState like rememberModalButtomSheetSTate(). Also I want to store composable inside this rememberState and able to set and access composable using this state. Like rememberState.setCompose = #Composable and rememberState.getComposable
Yes, you can create rememberable functions such as rememberModalBottomSheetState or rememberLazyListState. Creating custom remember functions that store multiple data type or MutableStates are common. Putting Composables is not common, at least i haven't seen so far, but it's doable.
class MyState internal constructor() {
var state1 by mutableStateOf(0)
var state2 by mutableStateOf(0)
var someValue = 0
}
internal constructor is optional, it's for restricting developer to create instance only by remember function.
// you can add params to pass to MyState instance or keys to reset remember
#Composable
fun rememberMyState(key1: Any?): MyState {
return remember(key1) {
MyState()
}
}
If you wish to add Composables one way of doing it is
class MyState internal constructor() {
var state1 by mutableStateOf(0)
var state2 by mutableStateOf(0)
var someValue = 0
// this can also be SnapshoStateList to trigger recomposition when you add, remove or update list with new item instance
val list = mutableListOf<#Composable () -> Unit>()
}
Usage
val myState = rememberMyState(key1 = Unit)
myState.list.add {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red)
)
}
myState.list.add {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Yellow)
)
}
Column(modifier = Modifier.fillMaxSize()) {
myState.list.forEach{
it.invoke()
}
}
Related
I am using compose LazyColumn with viewModel updating the list items by having inside my viewModel:
data class ContactsListUiState(
val contacts: MutableList<Contact>
)
#HiltViewModel
class ContactsViewModel #Inject constructor(savedStateHandle: SavedStateHandle) : ViewModel() {
private val _contactsListUiState = MutableStateFlow(ContactsListUiState(mutableListOf()))
val contactsListUiState: StateFlow<ContactsListUiState> = _contactsListUiState.asStateFlow()
private fun updateContactsList(newContacts: MutableList<Contact>) {
_contactsListUiState.update{ currentState ->
currentState.copy(
contacts = newContacts
)
}
}
such that my updateContactsList() function updates the stateFlow and I am supposed to collect the contacts list with collectAsStateWithLifecycle() inside my composable function inside MainActivity
#OptIn(ExperimentalLifecycleComposeApi::class, ExperimentalFoundationApi::class)
#Composable
fun ContactsListScreen(
navController: NavController,
modifier: Modifier = Modifier
) {
Log.d("ViewModel", "ContactsListScreen recomposed")
val uiState by contactsViewModel.contactsListUiState.collectAsStateWithLifecycle()
Box(modifier = modifier) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
Column {
TextField(
value = searchText,
singleLine = true,
onValueChange = {
searchText = it
Log.d("MainActivity", "calling filter with $it")
contactsViewModel.filterContactsByString(it)
},
leadingIcon = if (searchText.isNotBlank()) searchLeadingIcon else null,
trailingIcon = if (searchText.isBlank()) searchTrailingIcon else null,
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
)
LazyColumn(
Modifier.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(bottom = 80.dp)
) {
items(uiState.contacts) { contact ->
ContactListItem(
navController,
contact = contact,
modifier = Modifier.fillMaxWidth()
)
Divider(thickness = 0.5.dp, color = colorResource(id = R.color.blue))
}
}
...
where contactsListViewModel is declared on top of the activity which I inject using hilt :
#AndroidEntryPoint
class MainActivity : ComponentActivity(), LoaderManager.LoaderCallbacks<Cursor> {
private val contactsViewModel: ContactsViewModel by viewModels()
For some reason uiState.contacts is empty inside the composable function, but it does contain items when I logged it inside the viewModel function and therefore my list stays empty..
Any suggestions on what could have gone wrong?
Thanks
Please use SnapshotStatelist/(mutableStateListOf()) instead of an ordinary list (mutableList).
private val _contactsListUiState = MutableStateFlow(ContactsListUiState(mutableStateListOf()))
Also please check this post this one and this one
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)
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
}
)
#Composable functions are recomposed
if one the parameters is changed or
if one of the parameters is not #Stable/#Immutable
When passing items: List<Int> as parameter, compose always recomposes, regardless of List is immutable and cannot be changed. (List is interface without #Stable annotation). So any Composable function which accepts List<T> as parameter always gets recomposed, no intelligent recomposition.
How to mark List<T> as stable, so compiler knows that List is immutable and function never needs recomposition because of it?
Only way i found is wrapping like #Immutable data class ImmutableList<T>(val items: List<T>). Demo (when Child1 recomposes Parent, Child2 with same List gets recomposed too):
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeBasicsTheme {
Parent()
}
}
}
}
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val items = remember { listOf(1, 2, 3) }
Column {
// click forces recomposition of Parent
Child1(value = state.value,
onClick = { state.value = !state.value })
//
Child2(items)
}
}
#Composable
fun Child1(
value: Boolean,
onClick: () -> Unit
) {
Log.d("Test", "Child1 Draw")
Text(
"Child1 ($value): Click to recompose Parent",
modifier = Modifier
.clickable { onClick() }
.padding(8.dp)
)
}
#Composable
fun Child2(items: List<Int>) {
Log.d("Test", "Child2 Draw")
Text(
"Child 2 (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
You mainly have 2 options:
Use a wrapper class annotated with either #Immutable or #Stable (as you already did).
Compose compiler v1.2 added support for the Kotlinx Immutable Collections library.
With Option 2 you just replace List with ImmutableList.
Compose treats the collection types from the library as truly immutable and thus will not trigger unnecessary recompositions.
Please note: At the time of writing this, the library is still in alpha.
I strongly recommend reading this article to get a good grasp on how compose handles stability (plus how to debug stability issues).
Another workaround is to pass around a SnapshotStateList.
Specifically, if you use backing values in your ViewModel as suggested in the Android codelabs, you have the same problem.
private val _myList = mutableStateListOf(1, 2, 3)
val myList: List<Int> = _myList
Composables that use myList are recomposed even if _myList is unchanged. Opt instead to pass the mutable list directly (of course, you should treat the list as read-only still, except now the compiler won't help you).
Example with also the wrapper immutable list:
#Immutable
data class ImmutableList<T>(
val items: List<T>
)
var itemsList = listOf(1, 2, 3)
var itemsImmutable = ImmutableList(itemsList)
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val itemsMutableState = remember { mutableStateListOf(1, 2, 3) }
Column {
// click forces recomposition of Parent
Child1(state.value, onClick = { state.value = !state.value })
ChildList(itemsListState) // Recomposes every time
ChildImmutableList(itemsImmutableListState) // Does not recompose
ChildSnapshotStateList(itemsMutableState) // Does not recompose
}
}
#Composable
fun Child1(
value: Boolean,
onClick: () -> Unit
) {
Text(
"Child1 ($value): Click to recompose Parent",
modifier = Modifier
.clickable { onClick() }
.padding(8.dp)
)
}
#Composable
fun ChildList(items: List<Int>) {
Log.d("Test", "List Draw")
Text(
"List (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
#Composable
fun ChildImmutableList(items: ImmutableList<Int>) {
Log.d("Test", "ImmutableList Draw")
Text(
"ImmutableList (${items.items.size})",
modifier = Modifier
.padding(8.dp)
)
}
#Composable
fun ChildSnapshotStateList(items: SnapshotStateList<Int>) {
Log.d("Test", "SnapshotStateList Draw")
Text(
"SnapshotStateList (${items.size})",
modifier = Modifier
.padding(8.dp)
)
}
Using lambda, you can do this
#Composable
fun Parent() {
Log.d("Test", "Parent Draw")
val state = remember { mutableStateOf(false) }
val items = remember { listOf(1, 2, 3) }
val getItems = remember(items) {
{
items
}
}
Column {
// click forces recomposition of Parent
Child1(value = state.value,
onClick = { state.value = !state.value })
//
Child2(items)
Child3(getItems)
}
}
#Composable
fun Child3(items: () -> List<Int>) {
Log.d("Test", "Child3 Draw")
Text(
"Child 3 (${items().size})",
modifier = Modifier
.padding(8.dp)
)
}
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())
}
}
}