Can I wrap Modifier with remember in Jetpack Compose? - android

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
}
)

Related

Jetpack Compose: mutableStateOf doesn't update with flow

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)

How to create custom rememberState in jetpack compose?

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()
}
}

How to use sealed class data to display alphabet index scroller

After creating a sealed class for my LazyColumn, how can I use the inital of every item for an alphabet scroller? it.? is where my problem occurs as for some reason, it does not let me accesss my sealed class and use it, i.e. itemName.
sealed class Clothes {
data class FixedSizeClothing(val itemName: Int, val sizePlaceholder: Int): Clothes()
data class MultiSizeClothing(val itemName: Int, val sizePlaceholders:
List<Int>): Clothes()
}
val clothingItems = remember { listOf(
Clothes.FixedSizeClothing(itemName = R.string.jumper, itemPlaceholder = 8),
Clothes.MultiSizeClothing(itemName = R.string.dress, itemPlaceholders = listOf(0, 2))
)
}
val headers = remember { clothingItems.map { getString(it.?).first().uppercase() }.toSet().toList() }
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.padding(it)
) {
items(clothingItems) {
val text1 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = it.itemName)
is Clothes.MultiSizeClothing ->
stringResource(id = it.itemName)
}
val text2 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = R.string.size_placeholder, it.sizePlaceholder)
is Clothes.MultiSizeClothing ->
stringResource(id = R.string.size_placeholder_and_placeholder, it.itemPlaceholders[0], it.itemPlaceholders[1])
}
Column(modifier = Modifier
.fillMaxWidth()
.clickable {}) {...}
}
}
val offsets = remember { mutableStateMapOf<Int, Float>() }
var selectedHeaderIndex by remember { mutableStateOf(0) }
val scope = rememberCoroutineScope()
fun updateSelectedIndexIfNeeded(offset: Float) {
val index = offsets
.mapValues { abs(it.value - offset) }
.entries
.minByOrNull { it.value }
?.key ?: return
if (selectedHeaderIndex == index) return
selectedHeaderIndex = index
val selectedItemIndex = clothingItems.indexOfFirst { getString(it.?).first().uppercase() == headers[selectedHeaderIndex] }
scope.launch {
listState.scrollToItem(selectedItemIndex)
}
}
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
.background(Color.Gray)
.pointerInput(Unit) {
detectTapGestures {
updateSelectedIndexIfNeeded(it.y)
}
}
.pointerInput(Unit) {
detectVerticalDragGestures { change, _ ->
updateSelectedIndexIfNeeded(change.position.y)
}
}
) {
headers.forEachIndexed { i, header ->
Text(
header,
modifier = Modifier
.onGloballyPositioned {
offsets[i] = it.boundsInParent().center.y
},
color = Color.White
)
}
}
You can define the val itemName: Int in the parent Clothes class and override it in you other subclasses. If you do that, you then do not need to use a when expression if you just want to access the itemName property.
The parent class can be a sealed interface instead of a sealed class. That way it is a bit more flexible and a bit less verbose when overriding its properties. Also : Clothes() then becomes just : Clothes
sealed interface Clothes {
val itemName: Int
data class FixedSizeClothing(override val itemName: Int, val sizePlaceholder: Int): Clothes
data class MultiSizeClothing(override val itemName: Int, val sizePlaceholders: List<Int>): Clothes
}
And the line where you create the headers becomes
val headers = clothingItems.map { stringResource(it.itemName).first().uppercase() }.toSet().toList()
The remember {} does not make much sense because if your data changes so can the set of initial letters. You also cannot use the stringResouce() function inside remember {}, because stringResouce() has to be used inside a #Composable function.
There is a different way of obtaining Resources and then using resources.getString(...) if you would like to retrieve resource strings inside a remember {} block. But in this case the remember {} block does not make sense due to the data potentially changing. The optimization to cache initial letters would have to be done in a different way.

Jetpack Compose: Performance problem when element with GlideImage is clickable in LazyColumn

I have VideoListScreen with LazyColumn and as my item I use VideoItem. This LazyColumn it's created with grid items to have lazy grid view with Category header. Tag is tag of category. Category details is information about category colors, title etc.:
#Composable
fun VideoItem(
videoPath: String,
brush: Brush,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val assetFileDescriptor = LocalContext.current.assets.open(videoPath)
Surface(
modifier = modifier
.padding(5.dp)
.aspectRatio(1f)
.clickable { onClick() },
shape = Shapes.small,
elevation = 1.dp
) {
GlideImage(
imageModel = assetFileDescriptor.readBytes(),
contentScale = ContentScale.Crop,
requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE),
shimmerParams = ShimmerParams(
baseColor = MaterialTheme.colors.background,
highlightColor = Blue200,
durationMillis = 650,
dropOff = 0.65f,
tilt = 20f
)
)
Box(modifier = Modifier
.background(brush)
.fillMaxSize() )
}
}
VideoListScreen:
#Composable
fun VideoListScreen(
navController: NavHostController,
tag: String
) {
val cells = 2
val context = LocalContext.current
val categoryDetails = getCategoryDetailsBy(tag)
val videos = fetchVideos(context, tag)
LazyColumn(contentPadding = PaddingValues(5.dp)) {
item {
CategoryElement(
categoryDetails = categoryDetails,
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(5.dp),
customTitle = "O kategorii"
)
}
gridItems(videos, cells) { assetFileName ->
val videoPath = "$tag/$assetFileName"
VideoItem(
videoPath = videoPath,
brush = categoryDetails.transparentBrush
) { navController.navigateToPlayer(videoPath) } //onClick function
}
}
}
private fun fetchVideos(context: Context, tag: String): List<String> {
return context.resources.assets.list("$tag/")?.toList() ?: listOf()
}
gridItems extension function:
fun <T> LazyListScope.gridItems(
data: List<T>,
cells: Int,
itemContent: #Composable BoxScope.(T) -> Unit,
) {
items(data.chunked(cells)) { row ->
Row(Modifier.fillMaxWidth()) {
for ((index, item) in row.withIndex()) {
Box(Modifier.fillMaxWidth(1f / (cells - index))) {
itemContent.invoke(this, item)
}
}
}
}
}
The problem is that when I try to apply clickability on this item (no matter where) the thumbnail loading (from the assets) becomes almost twice as slow. What's interesting when onClick function is empty, performance issue disappearing. In function called "navigateToPlayer(videoPath)" I navigate to another screen and send "videoPath" with navController.
If you have any questions, feel free to ask!
In compose you're creating UI with view builders. This function can be called many times, when you start using animations it even can be recomposed on each frame.
That's why you shouldn't perform any heavy work directly in composable function. And if you do, you need to store the result so you don't need recalculation on the next recomposition.
Both fetchVideos and assets.open are quite heavy operations, and even result of getCategoryDetailsBy(not sure what's that) should be cached. To do that you need to use remember or rememberSaveable. Check out how are these different and more about state in composables.
So update your declarations like this:
val categoryDetails = remember { getCategoryDetailsBy(tag) }
val videos = remember { fetchVideos(context, tag) }
val context = LocalContext.current
val assetFileDescriptor = remember { context.assets.open(videoPath) }

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