Merge custom content in Composables - android-jetpack-compose

I have 2 custom components in Jetpack Compose:
#Composable
fun IconText(modifier: Modifier, icon: Int, text: String) {/**/}
and a grid where it is currently hardcoded:
#Composable
fun VerticalGrid( //fun <T> VerticalGrid(
data: List<Pair<Resource, Int>>, //List<T>,
columnCount: Int,
itemContent: #Composable () -> Unit
) {
val size = data.size
val rows = if (size == 0) 0 else 1 + (size - 1) / columnCount
for (rowIndex in 0..rows) {
val itemIndex = rowIndex * columnCount
Row {
val end = min(itemIndex + columnCount, size)
for (index in itemIndex until end) {
IconText(modifier = Modifier.weight(1f), data[index].first.image, data[index].second.toString())
}
}
}
}
How can I make the grid more customisable and change IconText to the custom itemContent?

You can do this by passing parameters to content lambda
#Composable
fun IconText(modifier: Modifier, icon: Int, text: String) {/**/
}
#Composable
fun VerticalGrid( //fun <T> VerticalGrid(
data: List<Pair<Resource, Int>>, //List<T>,
columnCount: Int,
itemContent: #Composable (Modifier, Int, String) -> Unit
) {
val size = data.size
val rows = if (size == 0) 0 else 1 + (size - 1) / columnCount
for (rowIndex in 0..rows) {
val itemIndex = rowIndex * columnCount
Row {
val end = min(itemIndex + columnCount, size)
for (index in itemIndex until end) {
itemContent(
Modifier.weight(1f),
data[index].first.image,
data[index].second.toString()
)
}
}
}
}
Usage
VerticalGrid(data = listOf(), columnCount = 10) { modifier, icon, text ->
IconText(modifier, icon, text)
}
Or
VerticalGrid(data = listOf(), columnCount = 10) { modifier, icon, text ->
Tex(modifier, text)
}
Or
VerticalGrid(data = listOf(), columnCount = 10) { modifier, icon, text ->
IconText(modifier, icon, text)
Tex(modifier, text)
}
With last one you need to consider how you set Modifier. Since it's a Row under the hood. If first Composable covers whole space second one won't be visible
With generic function
#Composable
fun <T> VerticalGrid(
data: List<T>,
columnCount: Int,
itemContent: #Composable (Modifier, T) -> Unit
) {
val size = data.size
val rows = if (size == 0) 0 else 1 + (size - 1) / columnCount
for (rowIndex in 0..rows) {
val itemIndex = rowIndex * columnCount
Row {
val end = min(itemIndex + columnCount, size)
for (index in itemIndex until end) {
itemContent(Modifier.weight(1f), data[index])
}
}
}
}
Usage
val list: List<Pair<Resource, Int>> = listOf(
Pair(Resource(1), 1)
)
VerticalGrid(
data = list,
columnCount = 10
) { modifier: Modifier, pair: Pair<Resource, Int> ->
IconText(
modifier = modifier,
icon = pair.first.image,
text = pair.second.toString()
)
}

Related

Dynamic Item width in Column (Jetpack Compose)

I have some LazyColumn (or Column) in Composable fun:
#Composable
fun MyColumn(items: List<Item>) {
val width = 0.dp // max for all amounts
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
Text(text = it.amount, modifier = Modifier.background(Color.Red).width(width))
}
}
}
}
class Item(val title: String, val amount: String)
How can I measure the width param? It must be equals the longest value for all amounts in items.
Finally I got some solution (I don't know is it correct or no, but it works):
I use SubcomposeLayout to measure width of whole separate column with amounts. Code of the measuring function:
#Composable
fun WithCalculateWidth(modifier: Modifier = Modifier,
contentForCalculate: #Composable () -> Unit,
dependentContent: #Composable (Dp) -> Unit) {
SubcomposeLayout(modifier = modifier) { constraints ->
val measuredWidth = subcompose("viewToMeasure", contentForCalculate)[0].measure(Constraints()).width.toDp()
val contentPlaceable = subcompose("content") { dependentContent(measuredWidth) }[0].measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
contentPlaceable.place(0, 0)
}
}
}
I use calculated width in my List:
#Composable
fun ListWithMeasuredAmounts(items: List<Item>) {
val allAmounts: #Composable () -> Unit = {
Column {
items.forEach {
AmountItem(amount = it.amount)
}
}
}
WithCalculateWidth(contentForCalculate = { allAmounts() }) { amountWidth ->
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
AmountItem(amount = it.amount, modifier = Modifier.width(amountWidth))
}
}
}
}
}
#Composable
fun AmountItem(amount: String, modifier: Modifier = Modifier) {
Text(text = amount, modifier = modifier.background(Color.Red))
}

Jetpack Compose LazyColumn Performance Issue

I'm writing example-screen with using lazyColumn. I encountered some performance issues on release build. Frame skipping happens when I fast-scroll the list.
All models and composables are stable. My code is below;
Screen record link -> https://imgur.com/a/cvlA8g0
viewModel:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val repo: ExampleRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(ItemsViewState())
val viewState = _viewState.asStateFlow()
init {
fetch()
}
private fun fetch() = viewModelScope.launch {
repo.getItems()
.onStart { _viewState.value = _viewState.value.copy(state = PageState.Loading) }
.onCompletion { _viewState.value = _viewState.value.copy(state = PageState.Content) }
.collect { _viewState.value = _viewState.value.copy(items = it.toImmutableList()) }
}
}
viewState and models:
data class ItemsViewState(
val items: ImmutableList<Item> = persistentListOf(),
val state: PageState = PageState.Loading,
)
data class Item(
val id: Int,
val imageUrl: String,
val name: String,
val rating: Double,
val campaignText: String,
val isChecked: Boolean = false,
)
data class ItemViewState(val item: Item) {
fun isRatingVisible(): Boolean = item.rating > 7.0
}
sealed class PageState {
object Content : PageState()
object Loading : PageState()
object Error : PageState()
}
and my composable functions:
#Composable
fun ExampleScreen(
viewModel: ExampleViewModel = hiltViewModel(),
) {
val viewState by viewModel.viewState.collectAsState()
when (viewState.state) {
PageState.Content -> {
ExampleList(viewState = viewState)
}
PageState.Loading -> LoadingScreen()
PageState.Error -> {}
}
}
#Composable
private fun ExampleList(
viewState: ItemsViewState,
) {
LazyColumn(
state = rememberLazyListState(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(viewState.items, key = { it.id }) { item ->
ExampleListItem(item = item)
}
}
}
#Composable
private fun ExampleListItem(item: Item) {
val viewState = ItemViewState(item)
Card(
shape = RoundedCornerShape(8.dp),
backgroundColor = MaterialTheme.colors.background
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.wrapContentHeight()
) {
AsyncImage(
model = item.imageUrl,
contentDescription = viewState.item.name,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.width(120.dp)
.height(120.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Column(verticalArrangement = Arrangement.SpaceBetween) {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Text(
text = viewState.item.name,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f),
)
Icon(imageVector = Icons.Default.List, contentDescription = null)
}
Spacer(modifier = Modifier.height(2.dp))
Row {
if (viewState.isRatingVisible()) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = viewState.item.rating.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
Spacer(modifier = Modifier.height(2.dp))
CampaignRow(campaignText = viewState.item.campaignText)
}
}
}
}
#Composable
private fun CampaignRow(
modifier: Modifier = Modifier,
campaignText: String,
) = Row(modifier = modifier) {
Image(
painter = painterResource(androidx.ui.livedata.R.drawable.abc_ic_star_black_16dp),
contentDescription = "",
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically)
.padding(end = 4.dp)
)
Text(
text = campaignText,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
I followed google guideline to fix performance issues.
I created Baseline profile
I tested on release build
I used key parameter on lazyColumn
but still happening performance issues. How can I prevent this?

How to create a JSpinner-like widget in Jetpack Compose

I would like to create a widget in Jetpack Compose (Desktop) with similar functionality as the JSpinner in Swing, i.e. an editable text field and two buttons that increase/decrease the value in the text field. Also, I would like
the value to be validated and to be saved when the spinner loses its focus
the buttons not to be skipped in the navigation, so that the user can navigate directly between multiple spinner text fields
After a lot of trial and error I have figured out the following working version, but I wonder if there is a simpler or more elegant way to do this:
#Composable
fun TextFieldSpinner(
label: #Composable (() -> Unit)?,
lastText: String,
validateText: (String) -> Boolean,
commitText: (String) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
val (isEditing, setEditing) = remember { mutableStateOf(false) }
// intermediateTextFieldValue is only used locally to store the temporary state of the TextField while editing
val (intermediateTextFieldValue, setIntermediateTextFieldValue) = remember { mutableStateOf(TextFieldValue(lastText))}
var isError by remember { mutableStateOf(!validateText(lastText)) }
val resetText = { setIntermediateTextFieldValue(TextFieldValue(lastText)) }
if (!isEditing && !isError && lastText != intermediateTextFieldValue.text) {
resetText()
}
val onCommit = {
if (validateText(intermediateTextFieldValue.text)) {
isError = false
commitText(intermediateTextFieldValue.text)
} else {
isError = true
}
}
val onLeaveTextField = {
setEditing(false)
onCommit()
isError = false
}
val onNewFocusState = { newFocusState: Boolean ->
setEditing(newFocusState)
if (!newFocusState)
onCommit()
}
}
TextFieldSpinnerUI(
label = label,
value = intermediateTextFieldValue,
onValueChange = { newTextFieldValue:TextFieldValue ->
setEditing(true)
setIntermediateTextFieldValue(newTextFieldValue)
// eager committing without showing error state
if (validateText(newTextFieldValue.text)) {
isError = false
commitText(newTextFieldValue.text)
}
},
modifier = modifier,
isError = isError,
onIncrement = {
onLeaveTextField()
onIncrement()
},
onDecrement = {
onLeaveTextField()
onDecrement()
},
onFocusChanged = { state ->
onNewFocusState(state.isFocused)
}
)
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun TextFieldSpinnerUI(
label: #Composable (() -> Unit)?,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
isError: Boolean,
onFocusChanged: (FocusState) -> Unit
) {
Row() {
DecreaseButton(onDecrement)
TextField(
label = label,
value = value,
singleLine = true,
isError = isError,
onValueChange = onValueChange,
modifier = Modifier.onFocusChanged(onFocusChanged)
)
IncreaseButton(onIncrement)
}
}
#Composable
private fun DecreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties {this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Remove
)
}
}
#Composable
private fun IncreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties { this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Add
)
}
}
In particular, it seems to be hard to have the text field at the same time
to be editable
to be validated, saved and then recomposed with the new value
to have another widget such as the buttons change its value

How can I show a composable on top of the visible Keyboard?

Since we have differen screen sizes and resolutions, I would like to place a Composable on top of unfolded keybard:
The keyboard (see above picture) is visible and I want to show nother composable (red square) like a message for a few seconds..
Using Jetpack Compose what would be a easy way to position that red composable?
This question can be solved using WindowInsets.isImeVisible however for it to return correct values you should set
WindowCompat.setDecorFitsSystemWindows(window, false)
i set this in Activity before setContent{}
Using WindowInsets.isImeVisible to check whether keyboard is open or not.
And we need to show message while keyboard is opening up so
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
is used for tracking whether keyboard is going up or down with LaunchedEffect
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Full implementation
#OptIn(ExperimentalLayoutApi::class)
#Composable
private fun TimedMessageLayout() {
val isImeVisible = WindowInsets.isImeVisible
var showMessage by remember { mutableStateOf(false) }
val density = LocalDensity.current
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomStart) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(WindowInsets.systemBars.asPaddingValues())
.border(2.dp, Color.Green)
) {
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null
)
var text by remember { mutableStateOf("") }
Text(
"Ime visible: ${WindowInsets.isImeVisible}, isKeyboardGoingDown: $isKeyboardGoingDown\n" +
"ime bottom: ${WindowInsets.ime.getBottom(density)}\n"
)
Spacer(modifier = Modifier.weight(1f))
TextField(
value = text,
onValueChange = { text = it }
)
}
if (showMessage && !isKeyboardGoingDown && offsetY != 0) {
Box(modifier = Modifier
.offset {
IntOffset(0, -offsetY)
}
.fillMaxWidth()
.height(200.dp)
.border(3.dp, Color.Red))
}
}
}
Result
Try this solution from here, I slightly modified
#Composable
fun keyboardHeightAsState(): State<Int> {
val keyboardHeight = remember { mutableStateOf(0) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardHeight.value = if (keypadHeight > screenHeight * 0.15) {
keypadHeight
} else {
0
}
}
}
return keyboardHeight
}
But I still have a small difference. I guess its the top bar, which I have to subtract

Click a close button on a bottomsheet to hide it in compose

I have the following composable bottomsheet.
I want to be able to close the bottomsheet either by dragging, clicking the background, and clicking the close button.
#Composable
fun CDSModelBottomSheet(toolBar: #Composable () -> Unit, content: #Composable () -> Unit) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar()
content()
}
}
) {}
}
#Composable
#Preview
fun PreviewCDSBottomSheet() {
CDSModelBottomSheet(
toolBar = { Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = {
/* close bottom sheet */
}
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_bottom_sheet),
tint = Color.Black,
)
}
})},
content = {
LoginMode()
}
)
}
In the trailingIcon I have an onClick event. But not sure how to trigger the bottomsheet to close. Unless I have to pass in the rememberModelBottomSheetState which I don't want to do.
This is the preview
Create a lambda to hide ModalBottomSheet as
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit = { coroutineScope.launch { sheetState.hide()} }
And pass this lambda as parameter to your content by updating toolbar as
toolbar: #Composable (() -> Unit) -> Unit
Full function
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CDSModelBottomSheet(
toolBar: #Composable (() -> Unit) -> Unit,
content: #Composable () -> Unit
) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit =
{ coroutineScope.launch { modelBottomSheetState.hide() } }
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar(hideModalBottomSheet)
content()
}
}
) {}
}
And use it as
CDSModelBottomSheet(
toolBar = { hide: () -> Unit ->
Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = hide
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Black,
)
}
}
)
},
content = {
LoginMode()
}
)

Resources