Introducing ComposeRecyclerView: The RecyclerView you know in Jetpack Compose

ComposeRecyclerView: Boost Jetpack Compose with RecyclerView's power.
Feb 7 2024 · 7 min read

Background

Jetpack Compose has revolutionized Android UI development with its declarative approach, enabling developers to build beautiful and efficient user interfaces with minimal code.

However, when it comes to displaying large lists of data, developers often encounter performance issues with the built-in LazyLists. 

ComposeRecyclerView comes to the rescue as a library that harnesses the power of RecyclerView within Jetpack Compose. 

Advantages of ComposeRecyclerView

  • Improved Performance: Ensures a smooth and responsive user experience, even with large datasets, effectively addressing LazyList performance concerns.
  • Drag-to-Reorder Support: Unlike Jetpack Compose’s lazy lists, it has built-in functionality for user-controlled reordering of list items.
  • Customization: Offers extensive options for tailoring RecyclerView behavior, providing developers complete control from layout orientation to item touch handling.
  • InfiniteScrollListener: Detects when users reach the list’s end during scrolling, facilitating manual pagination and additional data loading.
  • Seamless Integration: Effortlessly integrates RecyclerViews into Jetpack Compose apps.

Key Features

  • Efficient Item Generation: Adopts an on-demand Compose item generation approach as users scroll, for optimal memory usage and smooth scrolling — a concept akin to Flutter’s ListView.builder.
  • Flexible Item Builder: Empower developers to create rich and dynamic user experiences by defining custom UI layouts for individual list items using the item builder lambda function.
  • Direct RecyclerView Exposure: Allows direct access to RecyclerView features, for leveraging its ecosystem, including existing libraries, optimizations, and fine-grained control over item interactions.
  • Infinite Scrolling Support: Implementation of manual pagination and additional data loading upon reaching the list end.

Sponsored

Empower your journey to well-being with Justly. Your life is your greatest wealth — let us be your guide on the path to a happier you!

Introduction

In this post, we’ll delve into the implementation of ComposeRecyclerView, exploring its architecture, essential components, and inner workings.

ComposeRecyclerView is available on Github and MavenCentral

Check out a sample implementation of the ComposeRecyclerView library on GitHub, where you can find usage examples and see how it simplifies the RecyclerView integration with Jetpack Compose.

Architecture Overview

At its core, ComposeRecyclerView revolves around the familiar RecyclerView component, enhanced with Jetpack Compose’s composables. Let’s dissect its key components:

  1. ComposeRecyclerView Composable: The primary entry point responsible for rendering the RecyclerView and managing behaviors like scrolling, item generation, and drag-to-reorder.
  2. ComposeRecyclerViewAdapter: Tailored for ComposeRecyclerView, this RecyclerView adapter dynamically generates Compose items based on provided data and an item builder lambda function.
  3. ItemTouchHelperConfig: This class configures options for ItemTouchHelper, enabling developers to define custom drag-and-drop and swipe-to-dismiss behaviors.

ComposeRecyclerView Composable

The ComposeRecyclerView composable function serves as the backbone of the ComposeRecyclerView library, facilitating the creation of RecyclerViews with dynamically generated Compose items.

Let's delve into the implementation details of each parameter and callback:

Parameters

@Composable
fun ComposeRecyclerView(
    modifier: Modifier = Modifier,
    itemCount: Int,
    itemBuilder: @Composable (index: Int) -> Unit,
    onScrollEnd: () -> Unit = {},
    orientation: LayoutOrientation = LayoutOrientation.Vertical,
    itemTypeBuilder: ComposeRecyclerViewAdapter.ItemTypeBuilder? = null,
    onDragCompleted: (position: Int) -> Unit = { _ -> },
    itemTouchHelperConfig: (ItemTouchHelperConfig.() -> Unit)? = null,
    onItemMove: (fromPosition: Int, toPosition: Int, itemType: Int) -> Unit = { _, _, _ -> },
    onCreate: (RecyclerView) -> Unit = {}
) {
    // Implementation...
}
  • modifier: A parameter of type Modifier that allows customization of the RecyclerView's appearance and behavior.
  • itemCount: An integer parameter representing the total number of items to be displayed in the RecyclerView.
  • itemBuilder: A lambda parameter that defines the Compose content for each item based on its index.
  • Other parameters: Additional parameters for callbacks, orientation, item type handling, drag-and-drop support, and RecyclerView customization.

Remember State

var scrollState by rememberSaveable { mutableStateOf(bundleOf()) }
  • Declares a mutable state variable, scrollState, using rememberSaveable, which preserves scroll value across recompositions.

LayoutManager Initialization

val layoutManager = remember {
    val layoutManager = LinearLayoutManager(context)
    layoutManager.onRestoreInstanceState(scrollState.getParcelable("RecyclerviewState"))
    // Layout manager configuration based on orientation...
    layoutManager.orientation = when (orientation) {
        LayoutOrientation.Horizontal -> RecyclerView.HORIZONTAL
        LayoutOrientation.Vertical -> RecyclerView.VERTICAL
    }
    layoutManager
}
  • Initializes the RecyclerView’s layout manager using LinearLayoutManager.
  • Configures the layout manager based on the specified orientation (horizontal or vertical).

Adapter Initialization

val adapter = remember {
    ComposeRecyclerViewAdapter().apply {
        this.totalItems = itemCount
        this.itemBuilder = itemBuilder
        itemTypeBuilder?.let {
           this.itemTypeBuilder = itemTypeBuilder
        }
        this.layoutOrientation = orientation
    }
}
  • Initializes the RecyclerView adapter using ComposeRecyclerViewAdapter.
  • Configures the adapter based on the provided parameters, such as item count, item builder, and orientation.

RecyclerView Creation

val composeRecyclerView = remember {
    RecyclerView(context).apply {
        this.layoutManager = layoutManager
        this.clipToOutline = true
        addOnScrollListener(object : InfiniteScrollListener() {
          override fun onScrollEnd() {
            onScrollEnd()
          }
        })
        this.adapter = adapter
    }
}
  • Initializes the RecyclerView using RecyclerView.
  • Configures the RecyclerView, including layout manager, clip to outline, scroll listener, and adapter assignment.

ItemTouchHelper Initialization

val config = remember {
    ItemTouchHelperConfig().apply { itemTouchHelperConfig?.invoke(this) }
}
val itemTouchHelper = remember {
    ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
        config.dragDirs ?: (UP or DOWN or START or END), config.swipeDirs ?: (LEFT or RIGHT)
    ) {
        // Callbacks for drag and swipe interactions...
    })
}
  • Initializes the config, itemTouchHelpervariables using remember, ensuring that its state is preserved across recompositions.
  • Constructs an ItemTouchHelperConfig object and applies any specified configurations through the lambda itemTouchHelperConfig.
  • Configures drag and swipe directions based on the config object's properties, with defaults set to allow movement in all directions for drag and swipe.

AndroidView Integration

AndroidView(
    factory = {
        composeRecyclerView.apply {
            onCreate.invoke(this)
            itemTypeBuilder?.let {
                itemTouchHelper.attachToRecyclerView(this)
            }
        }
    },
    modifier = modifier,
    update = {
        adapter.update(itemCount, itemBuilder, orientation, itemTypeBuilder)
    }
)
  • Embeds the RecyclerView within the Compose UI using AndroidView.
  • Applies any custom initialization logic through the onCreate callback.
  • Attaches the itemTouchHelper to the RecyclerView if itemTypeBuilder is provided, enabling drag-and-drop functionality.
  • Updates the adapter with the latest item count, item builder, orientation, and item type builder.

DisposableEffect for State Preservation

DisposableEffect(key1 = Unit, effect = {
    onDispose {
        scrollState = bundleOf("RecyclerviewState" to layoutManager.onSaveInstanceState())
    }
})
  • Saves the RecyclerView’s scroll state using onSaveInstanceState() to preserve its position across configuration changes.

The final code is available on github!

ComposeRecyclerViewAdapter

It plays a pivotal role in managing dynamically generated Compose items within RecyclerViews.

Acting as a bridge between RecyclerViews and Compose, offering a streamlined approach to managing dynamically generated Compose items within RecyclerViews.

This adapter simplifies the process of populating RecyclerViews with Compose content, enabling developers to create highly customizable and performant UIs.

In this section, we’ll dive into the detailed implementation of it, exploring its key features and functionalities.

/**
 * RecyclerView adapter for handling dynamically generated Compose items.
 */
class ComposeRecyclerViewAdapter :
    RecyclerView.Adapter<ComposeRecyclerViewAdapter.ComposeRecyclerViewHolder>(){
    // Interface to define the method for determining item type
    interface ItemTypeBuilder {
        fun getItemType(position: Int): Int
    }
    // List to hold the items in the RecyclerView
    private var itemList: MutableList<Any> = mutableListOf()
    // Total number of items in the RecyclerView
    var totalItems: Int = 0
        set(value) {
            if (field == value) return
            field = value
            // Notify the adapter about changes in item count
            notifyItemRangeChange(value)
        }
    // Lambda function to build each item in the RecyclerView
    var itemBuilder: (@Composable (index: Int) -> Unit)? =
        null
    // Interface implementation to determine item type
    var itemTypeBuilder: ItemTypeBuilder? = null
    // Layout orientation of the RecyclerView
    var layoutOrientation: LayoutOrientation = LayoutOrientation.Vertical
        set(value) {
            if (field == value) return
            field = value
            // Notify the adapter about changes in layout orientation
            notifyItemChanged(0)
        }
    // ViewHolder class to hold ComposeView
    inner class ComposeRecyclerViewHolder(val composeView: ComposeView) :
        RecyclerView.ViewHolder(composeView)
    // Called when RecyclerView needs a new ViewHolder of the given type to represent an item
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ComposeRecyclerViewHolder {
        val context = parent.context
        val composeView = ComposeView(context)
        return ComposeRecyclerViewHolder(composeView)
    }
    // Called by RecyclerView to display the data at the specified position
    override fun onBindViewHolder(holder: ComposeRecyclerViewHolder, position: Int) {
        holder.composeView.apply {
            tag = holder
            // Set the content of the ComposeView using the provided lambda function itemBuilder
            setContent {
                itemBuilder?.invoke(position)
            }
        }
    }
    // Returns the total number of items held by the adapter
    override fun getItemCount(): Int = totalItems
    // Returns the view type of the item at the specified position
    override fun getItemViewType(position: Int): Int {
        return itemTypeBuilder?.getItemType(position) ?: 0
    }
    // Notifies the adapter about changes in item count with optimized range change notifications
    private fun notifyItemRangeChange(newSize: Int) {
        val oldSize = itemList.size
        if (newSize < oldSize) {
            // Remove excess items from the list and notify corresponding range removed
            itemList = itemList.subList(0, newSize)
            notifyItemRangeRemoved(newSize, oldSize - newSize)
        } else if (newSize > oldSize) {
            // Add new items to the list and notify corresponding range inserted
            val list = MutableList(newSize - oldSize) { Any() }
            itemList = (itemList + list).toMutableList()
            notifyItemRangeInserted(oldSize, newSize - oldSize)
        }
    }
}

The final code is available on github!

ItemTouchHelperConfig

This class empowers developers with the ability to finely tune the behavior of drag-and-drop and swipe actions within a ComposeRecyclerView.

With its set of configurable callbacks and flags, it offers a versatile solution for tailoring the user interaction experience to meet specific application requirements.

Let's delve into its key features:

class ItemTouchHelperConfig {
    var nonDraggableItemTypes: Set<Int> = emptySet()
    var onMove: ((recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) -> Boolean)? = null
    var onSwiped: ((viewHolder: RecyclerView.ViewHolder, direction: Int) -> Unit)? = null
    var clearView: ((recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) -> Unit)? = null
    var getMovementFlags: ((recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) -> Int)? = null
    var onSelectedChanged: ((viewHolder: RecyclerView.ViewHolder?, actionState: Int) -> Unit)? = null
    var isLongPressDragEnabled: Boolean = true
    var swipeDirs: Int? = null
    var dragDirs: Int? = null
}
  • nonDraggableItemTypes: Define a set of item types that should not be draggable, ensuring precise control over which items can be rearranged.
  • onMove: Customize the behavior for determining whether a ViewHolder can be moved to a new position, enabling complex logic for reordering items.
  • onSwiped: Implement custom actions for handling swipe-to-dismiss behavior, providing seamless interaction for removing items from the list.
  • clearView: Seamlessly manage the cleanup of views after they are moved or swiped, maintaining a polished user interface.
  • getMovementFlags: Dynamically provide movement flags for drag and swipe behavior, allowing for granular control over the types of interactions supported.
  • onSelectedChanged: Handle changes in the selection state of an item, enabling dynamic updates to the UI based on user actions.
  • isLongPressDragEnabled: Toggle the long press drag behavior on or off, offering flexibility in how drag actions are initiated.
  • swipeDirs and dragDirs: Specify the swipe and drag directions for items, enabling customization of the allowed movement directions within the RecyclerView.

Utilizing the capabilities of the ItemTouchHelperConfig, developers can craft immersive and intuitive user experiences that seamlessly align with their app’s design and functionality.

Whether implementing intricate drag-and-drop gestures or fine-tuning swipe-to-dismiss actions, this configuration class empowers developers with the tools to unlock endless possibilities for interaction customization.

Get Started with ComposeRecyclerView

Ready to elevate your Jetpack Compose apps with high-performance RecyclerViews? 

Get started with ComposeRecyclerView today by integrating it into your project and exploring its rich feature set. Experience the difference in performance and user interaction that ComposeRecyclerView brings to your Android app development workflow.

Stay tuned for updates, enhancements, and new features as we continue to evolve ComposeRecyclerView to meet the needs of modern Android app development. 

Happy coding! 🚀

Conclusion

ComposeRecyclerView emerges as a powerful solution, delivering heightened performance, drag-to-reorder functionality, and effortless integration with Jetpack Compose.

Whether you’re building a social media feed, a task manager, or a product catalog, ComposeRecyclerView empowers developers to deliver fluid and responsive user experiences.

Related Article


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
megh-l image
Megh Lath
Android developer | Sharing knowledge of Jetpack Compose & android development
megh-l image
Megh Lath
Android developer | Sharing knowledge of Jetpack Compose & android development
canopas-logo
We build products that customers can't help but love!
Get in touch
background-image

Get started today

Let's build the next
big thing!

Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.

Get Free Consultation
footer
Follow us on
2024 Canopas Software LLP. All rights reserved.