Jetpack Compose turns upside-down Android UI development with its modern, declarative slant. Amid its many powerful features, modifiers emerge as a crucial tool for developers. They allow the customization and extension of composable functions.
Modifiers are not just limited to pre-established functionalities; they can be stretched out and customized further. This flexibility is a game-changer for Android developers, allowing for unprecedented UI manipulation and behavior customization.
Building upon our previous exploration of basic Jetpack Compose modifiers, we’ll explore the advanced modifiers that elevate Jetpack Compose from a simple UI toolkit to a sophisticated development framework. These modifiers go beyond basic styling, offering developers powerful control and flexibility in creating dynamic, responsive interfaces.
Let’s start with the following use case: We need to build a photo grid layout, displaying the thumbnail, allowing the selection of multiple photos after a long press, or showing the photo details by just tapping on the photo item.

We need to define the desired behavior for photo interactions:
The critical problem is managing these interactions effectively. Since both actions rely on similar input methods (single press and long press) using a single modifier, they risk conflicting with each other. This means one action could potentially prevent or interfere with the other, creating a user experience challenge that requires a carefully designed solution.
The key is to implement a mechanism that can distinguish between these interaction types without mutual interference, ensuring both functionalities can coexist smoothly.
Our first objective is to implement a long press mechanism that toggles selection mode, enabling custom actions like deleting or sharing. We’ll achieve this by extending the pointerInput modifier to create a specialized interaction handler tailored to our specific use case.
fun Modifier.multiSelectGridHandler(
lazyGridState: LazyGridState,
haptics: HapticFeedback,
selectedIds: MutableState<Set<Long>>,
autoScrollSpeed: MutableState<Float>,
autoScrollThreshold: Float
) = pointerInput(Unit) {
fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset): Long? =
layoutInfo.visibleItemsInfo.find { itemInfo ->
itemInfo
.size
.toIntRect()
.contains(hitPoint.round() - itemInfo.offset)
}?.key as? Long
var initialKey: Long? = null
var currentKey: Long? = null
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
lazyGridState.gridItemKeyAtPosition(offset)?.let { key ->
if (!selectedIds.value.contains(key)) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
initialKey = key
currentKey = key
selectedIds.value += key
}
}
},
onDragCancel = { initialKey = null; autoScrollSpeed.value = 0f },
onDragEnd = { initialKey = null; autoScrollSpeed.value = 0f },
onDrag = { change, _ ->
if (initialKey != null) {
val distFromBottom =
lazyGridState.layoutInfo.viewportSize.height - change.position.y
val distFromTop = change.position.y
autoScrollSpeed.value = when {
distFromBottom < autoScrollThreshold
-> autoScrollThreshold - distFromBottom
distFromTop < autoScrollThreshold
-> -(autoScrollThreshold - distFromTop)
else -> 0f
}
lazyGridState.gridItemKeyAtPosition(change.position)?.
let { key ->
if (currentKey != key) {
selectedIds.value = selectedIds.value
.minus(initialKey!!..currentKey!!)
.minus(currentKey!!..initialKey!!)
.plus(initialKey!!..key)
.plus(key..initialKey!!)
currentKey = key
}
}
}
}
)
}
This custom modifier is specifically designed for a
LazyVerticalGridlayout. Consequently, it leverages the unique properties ofLazyGridStateand other grid-specific parameters to enable precise item selection and interaction within the grid’s context.
This extension function multiSelectGridHandler enhances the pointerInput modifier for a lazy grid with multi-select drag functionality. Key features include:
Grid Item Detection:
gridItemKeyAtPosition finds the grid item at a specific touch point.Long Press Selection:
selectedIds.Drag Selection:
selectedIds set.Auto-Scrolling:
The function provides a sophisticated, custom interaction handler for grid-based selection scenarios.
Differentiating between selection and non-selection modes is crucial for implementing context-specific actions. The then() modifier provides an elegant solution for conditionally applying interaction behaviors.
By using then(), we can dynamically adjust the modifier stack based on the current selection state, enabling precise control over user interactions. This approach allows us to:
The implementation will leverage the then() modifier to switch between standard and selection-mode interactions seamlessly.
Let’s bridge the connection between the multiSelectGridHandler extension function and the then() modifier implementation.
Our custom multiSelectGridHandler extension function provides advanced multi-select drag capabilities for a LazyVerticalGrid. The then() modifier complements this by dynamically applying selection behavior based on the current interaction mode.
Implementation Highlight:
@Composable
fun PhotosGrid(
photos: List<Photo> = emptyList(),
selectedIds: MutableState<Set<Long>> = rememberSaveable { mutableStateOf(emptySet()) },
onPhotoClicked: (Photo) -> Unit = {}
) {
val inSelectionMode by remember {
derivedStateOf { selectedIds.value.isNotEmpty() }
}
val lazyGridState = rememberLazyGridState()
val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
LaunchedEffect(autoScrollSpeed.floatValue) {
if (autoScrollSpeed.floatValue != 0f) {
while (isActive) {
lazyGridState.scrollBy(autoScrollSpeed.floatValue)
delay(10)
}
}
}
LazyVerticalGrid(
state = lazyGridState,
columns = GridCells.Adaptive(minSize = 128.dp),
verticalArrangement = Arrangement.spacedBy(3.dp),
horizontalArrangement = Arrangement.spacedBy(3.dp),
modifier = Modifier
.photoGridDragHandler( // Extension function Implementation
lazyGridState = lazyGridState,
haptics = LocalHapticFeedback.current,
selectedIds = selectedIds,
autoScrollSpeed = autoScrollSpeed,
autoScrollThreshold = with(
LocalDensity.current
) { 40.dp.toPx() }
)
) {
items(
items = photos,
key = { it.id },
) { photo ->
val selected by remember {
derivedStateOf { selectedIds.value.contains(photo.id) }
}
// Custom composable to display the photo item on the grid layout.
ImageItem(
photo = photo,
inSelectionMode = inSelectionMode,
selected = selected,
modifier = Modifier
.semantics {
if (!inSelectionMode) {
onLongClick("Select") {
selectedIds.value += photo.id
true
}
}
}
.clickable(onClick = { onPhotoClicked(photo) })
.then(
if (inSelectionMode) {
Modifier
.toggleable(
value = selected,
interactionSource = remember {
MutableInteractionSource()
},
indication = null,
onValueChange = {
if (it) {
selectedIds.value += photo.id
} else {
selectedIds.value -= photo.id
}
}
)
} else Modifier
)
)
}
}
}
While advanced modifiers offer vast flexibility, developers must consider potential performance implications.
Custom modifier implementations like multiSelectGridHandler involve complex gesture detection and state management, which can introduce computational overhead. It’s crucial to profile and optimize these interactions, especially in grid layouts with large numbers of items. Techniques such as using remember and derivedStateOf help minimize unnecessary recompositions, while carefully designed gesture handlers can reduce the computational complexity of interaction detection.
Moreover, leveraging Compose’s built-in state management and composition model ensures that these advanced interaction patterns remain efficient and responsive, even in complex UI scenarios. The key is to balance rich interactivity and computational efficiency, using modifiers as a precise instrument for crafting sophisticated user experiences.
The power of Jetpack Compose’s modifiers extends far beyond simple styling. As demonstrated in our previous example, modifiers are a versatile tool that enables developers to create complex, interactive user interfaces with remarkable flexibility.
By leveraging advanced techniques like then() and custom extension functions, developers can craft nicety interaction patterns that respond intelligently to user gestures.
The ability to dynamically apply and combine modifiers allows for creating responsive, context-aware UI components that can seamlessly transition between different interaction modes. This approach not only enhances user experience but also promotes cleaner, and and more maintainable code by separating interaction logic and UI rendering