{"id":13428749,"url":"https://github.com/afollestad/recyclical","last_synced_at":"2026-01-14T03:31:20.395Z","repository":{"id":57716595,"uuid":"174766902","full_name":"afollestad/recyclical","owner":"afollestad","description":"🚀 An easy-to-use, extensible Kotlin DSL for setting up and manipulating RecyclerViews.","archived":true,"fork":false,"pushed_at":"2022-12-24T21:51:22.000Z","size":2201,"stargazers_count":714,"open_issues_count":12,"forks_count":62,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-11-01T01:02:12.216Z","etag":null,"topics":["android","androidx","dsl","kotlin","list-selection","lists","recyclerview","user-interface"],"latest_commit_sha":null,"homepage":"https://af.codes","language":"Kotlin","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/afollestad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"afollestad","ko_fi":"afollestad"}},"created_at":"2019-03-10T01:58:18.000Z","updated_at":"2025-09-09T14:58:02.000Z","dependencies_parsed_at":"2022-08-23T21:10:44.366Z","dependency_job_id":null,"html_url":"https://github.com/afollestad/recyclical","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/afollestad/recyclical","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afollestad%2Frecyclical","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afollestad%2Frecyclical/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afollestad%2Frecyclical/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afollestad%2Frecyclical/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/afollestad","download_url":"https://codeload.github.com/afollestad/recyclical/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afollestad%2Frecyclical/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","androidx","dsl","kotlin","list-selection","lists","recyclerview","user-interface"],"created_at":"2024-07-31T01:01:04.160Z","updated_at":"2026-01-14T03:31:20.374Z","avatar_url":"https://github.com/afollestad.png","language":"Kotlin","funding_links":["https://github.com/sponsors/afollestad","https://ko-fi.com/afollestad"],"categories":["Libraries"],"sub_categories":[],"readme":"# Recyclical\n\n*recyclical*: an easy-to-use, extensible Kotlin DSL for setting up and manipulating RecyclerViews.\n\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bdc552fb3832423986a296a47b9ddef0)](https://www.codacy.com/app/drummeraidan_50/recyclical?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=afollestad/recyclical\u0026amp;utm_campaign=Badge_Grade)\n[![Android CI](https://github.com/afollestad/recyclical/workflows/Android%20CI/badge.svg)](https://github.com/afollestad/recyclical/actions?query=workflow%3A%22Android+CI%22)\n[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html)\n\n\u003cimg src=\"https://raw.githubusercontent.com/afollestad/recyclical/master/art/showcase3.png\" width=\"600\" /\u003e\n\n---\n\nThis repo is archived as this library is deprecated. I do not wish to maintain a View-based library now that Jetpack Compose is a thing. Prefer `LazyColumn`/`LazyRow`.\n\n# Table of Contents\n\n## Core\n\n1. [Gradle Dependency](#gradle-dependency)\n2. [The Basics](#the-basics)\n3. [More Options](#more-options)\n4. [Child View Clicks](#child-view-clicks)\n5. [Multiple Item Types](#multiple-item-types)\n6. [DataSource](#datasource)\n    1. [Construction](#construction)\n    2. [Manipulation](#manipulation)\n    4. [Diffing](#diffing)\n7. [SelectableDataSource](#selectabledatasource)\n    1. [Construction](#construction-1)\n    2. [Manipulation](#manipulation-1)\n    3. [Use in Binding](#use-in-binding)\n8. [Stable IDs](#stable-ids)\n\n## Swipe\n\n1. [Gradle Dependency](#gradle-dependency-1)\n2. [The Basics](#the-basics-1)\n3. [Long Swipes](#long-swipes)\n4. [Customization](#customization)\n\n---\n\n# Core\n\n[ ![Core](https://img.shields.io/maven-central/v/com.afollestad/recyclical?style=flat\u0026label=Core) ](https://repo1.maven.org/maven2/com/afollestad/recyclical)\n\n## Gradle Dependency\n\nAdd this to your module's `build.gradle` file:\n\n```gradle\ndependencies {\n\n  implementation 'com.afollestad:recyclical:1.1.1'\n}\n```\n\n---\n\n## The Basics\n\n**First**, declare an Item class:\n\n```kotlin\ndata class Person(\n  var name: String,\n  var arg: Int\n)\n```\n\n**Second**, a layout and a View Holder:\n\n```xml\n\u003cLinearLayout ...\u003e\n\n  \u003cTextView \n     android:id=\"@+id/text_name\"\n     ... /\u003e    \n     \n  \u003cTextView \n     android:id=\"@+id/text_age\"\n     ... /\u003e\n     \n\u003c/LinearLayout\u003e\n```\n\n```kotlin\nclass PersonViewHolder(itemView: View) : ViewHolder(itemView) {\n  val name: TextView = itemView.findViewById(R.id.text_name)\n  val age: TextView = itemView.findViewById(R.id.text_age)\n}\n```\n\n**Finally**, you can begin using the DSL API:\n\n```kotlin\nclass MainActivity : AppCompatActivity() {\n\n  override fun onCreate(savedInstanceState: Bundle?) {\n      super.onCreate(savedInstanceState)\n      \n      // dataSourceTypedOf(...) here creates a DataSource\u003cPerson\u003e\n      val dataSource = dataSourceTypedOf(\n          Person(\"Aidan\", 24),\n          Person(\"Nina\", 24)\n      )\n      \n      // setup{} is an extension method on RecyclerView\n      recyclerView.setup {\n          withDataSource(dataSource)\n          withItem\u003cPerson, PersonViewHolder\u003e(R.layout.person_item_layout) {\n            onBind(::PersonViewHolder) { index, item -\u003e\n              // PersonViewHolder is `this` here\n              name.text = item.name\n              age.text = \"${item.age}\"\n            }\n            onClick { index -\u003e\n              // item is a `val` in `this` here\n              toast(\"Clicked $index: ${item.name}\")\n            }\n            onLongClick { index -\u003e\n              // item is a `val` in `this` here \n              toast(\"Long clicked $index: ${item.name}\")\n            }\n         }\n      }\n  }\n}\n```\n\n---\n\n## More Options\n\nThere are other things you can give to the setup extension:\n\n```kotlin\nrecyclerView.setup {\n  // Custom layout manager, rather than the default which is a vertical LinearLayoutManager\n  withLayoutManager(GridLayoutManager(context, 2))\n  // Assigns a view that is made visible when the data source has no content, else is hidden (gone)\n  withEmptyView(view)\n  // Global click listener for any item type. Individual item click listeners are called first.\n  withClickListener { index, item -\u003e }\n  // Global long click listener for any item type. Individual item long click listeners are called first.\n  withLongClickListener { index, item -\u003e }\n  // Add an animation used to animate the group's children after the first layout.\n  withLayoutAnimation(R.anim.your_anim, durationRes = android.R.integer.config_animShortTime)\n}\n```\n\n---\n\n## Child View Clicks\n\nThere are many cases in which you'd want to get callbacks for a child view in your list items \ngetting clicked, such as the sender icon in a list of emails.\n\n```kotlin\nclass EmailViewHolder(itemView: View) : ViewHolder(itemView) {\n  val icon = itemView.findViewById\u003cImageView\u003e(R.id.icon)\n}\n\nrecyclerView.setup {\n  withItem\u003cEmailItem, EmailViewHolder\u003e(R.layout.email_item_layout) {\n    ...\n    onChildViewClick(EmailViewHolder::icon) { index, view -\u003e\n      // `this` includes `item` along with selection-related methods discussed below in SelectableDataSource\n      // `view` argument here is automatically an `ImageView`\n    }\n  }\n}\n```\n\n---\n\n## Multiple Item Types\n\nYou can mix different types of items - but you need to specify view holders and layouts for them too:\n\n```kotlin\n// dataSourceOf(...) without \"typed\" creates a DataSource\u003cAny\u003e\nval dataSource = dataSourceOf(\n  Car(2012, \"Volkswagen GTI\"),\n  Motorcycle(2018, \"Triumph\", \"Thruxton R\"),\n  Person(\"Aidan\", 24)\n)\n\nrecyclerView.setup {\n  withDataSource(dataSource)\n  withItem\u003cPerson, PersonViewHolder\u003e(R.layout.person_item_layout) {\n     onBind(::PersonViewHolder) { index, item -\u003e\n        name.text = item.name\n        age.text = \"${item.age}\"\n     }\n  }\n  withItem\u003cMotorcycle, MotorcycleViewHolder\u003e(R.layout.motorcycle_item_layout) {\n     onBind(::MotorcycleViewHolder) { index, item -\u003e\n        year.text = \"${item.year}\"\n        make.text = item.make\n        model.text = item.model\n     }\n  }\n  withItem\u003cCar, CarViewHolder\u003e(R.layout.car_item_layout) {\n     onBind(::CarViewHolder) { index, item -\u003e\n        year.text = \"${item.year}\"\n        name.text = item.name\n     } \n  }\n}\n```\n\n---\n\n## DataSource\n\n`DataSource` is an interface which provides data and allows manipulation of the data, to display in a RecyclerView. \nBeing an interface means you make your own implementations of it, you can mock it in tests, you could even provide it \nvia Dagger to a presenter and manipulate the RecyclerView outside of your UI layer.\n\n### Construction\n\nThe included implementation of data source operates on a List of objects (of any type).\n\n```kotlin\n// Empty by default, but can still add, insert, etc.\nval dataSource: DataSource\u003cAny\u003e = emptyDataSource()\nval dataSourceTyped: DataSource\u003cPerson\u003e = emptyDataSourceTyped\u003cPerson\u003e() \n \n\n// Initial data set of items from a vararg list\nval dataSource: DataSource\u003cAny\u003e = dataSourceOf(item1, item2)\nval dataSourceTyped: DataSource\u003cPerson\u003e = dataSourceTypedOf(item1, item2)\n\n// Initial data set of items from an existing list\n// Could also use dataSourceTypedOf(...)\nval items = listOf(item1, item2)\nval dataSource: DataSource\u003cAny\u003e = dataSourceOf(items)\nval dataSourceTyped: DataSource\u003cPerson\u003e = dataSourceTypedOf(items)\n\n```\n\n### Manipulation\n\n```kotlin\nval dataSource: DataSource\u003cItemType\u003e = // ...\n\n// getters\nval item: ItemType = dataSource[5]\nval contains: Boolean = dataSource.contains(item)\nval size: Int = dataSource.size()\nval isEmpty: Boolean = dataSource.isEmpty()\nval isNotEmpty: Boolean = dataSource.isNotEmpty()\nval firstIndex: Int = dataSource.indexOfFirst { }\nval lastIndex: Int = dataSource.indexOfLast { }\n\n// mutation\nval person = Person(\"Aidan\", 24)\ndataSource.add(person)\ndataSource.set(listOf(person))\ndataSource.insert(1, person)\ndataSource.removeAt(1)\ndataSource.remove(person)\ndataSource.swap(1, 4)\ndataSource.move(1, 4)\ndataSource.clear()\n\n// iteration\nfor (item in dataSource) { }\ndataSource.forEach { }  // emits all items\ndataSource.forEachOf\u003cPerson\u003e { }  // only emits items that are a Person\n\n// operators\nval item: Any = dataSource[5]  // get(5)\nval contains: Boolean = item in dataSource  // contains(item)\ndataSource += person  // add(person)\ndataSource -= person  // remove(person)\n```\n\n### Diffing\n\nWhen performing a `set` on the data set, you can opt to use diff utils:\n\n```kotlin\ndataSource.set(\n  newItems = newItems,\n  areTheSame = ::areItemsTheSame,\n  areContentsTheSame = ::areItemContentsTheSame\n)\n\n// Return true if items represent the same entity, e.g. by ID or name\nprivate fun areItemsTheSame(left: Any, right: Any): Boolean {\n  return when (left) {\n    is Person -\u003e {\n      right is Person \u0026\u0026 right.name == left.name\n    }\n    else -\u003e false\n  }\n}\n\n// Return true if all contents in the items are equal\nprivate fun areItemContentsTheSame(left: Any, right: Any): Boolean {\n  return when (left) {\n    is Person -\u003e {\n      right is Person \u0026\u0026\n        right.name == left.name \u0026\u0026\n        right.age == left.age\n    }\n    else -\u003e false\n  }\n}\n```\n\nThis will automatically coordinate notifying of adds, moves, and insertions so that \nupdate of the data set is pretty and animated by the RecyclerView.\n\n---\n\n## SelectableDataSource\n\nA `SelectableDataSource` is built on top of a regular [DataSource]. It provides additional APIs \nto manage the selection state of items in your list.\n\n### Construction\n\nConstruction methods for `SelectableDataSource` are the same as the `DataSource` ones, they just \ninclude `selectable` in their names.\n\n```kotlin\n// Empty by default, but can still add, insert, etc.\n// Could also use emptySelectableDataSourceTyped()\nval dataSource: SelectableDataSource\u003cAny\u003e = emptySelectableDataSource()\nval dataSourceTyped: SelectableDataSource\u003cPerson\u003e = emptySelectableDataSourceTyped()\n\n// Initial data set of items from a vararg list\n// Could also use selectableDataSourceTypedOf(...)\nval dataSource: SelectableDataSource\u003cAny\u003e = selectableDataSourceOf(item1, item2)\nval dataSourceTyped: SelectableDataSource\u003cPerson\u003e = selectableDataSourceTypedOf(item1, item2)\n\n// Initial data set of items from an existing list\n// Could also use selectableDataSourceTypedOf(...)\nval items = listOf(item1, item2)\nval dataSource: SelectableDataSource\u003cAny\u003e = selectableDataSourceOf(items)\nval dataSourceTyped: SelectableDataSource\u003cPerson\u003e = selectableDataSourceTypedOf(items)\n```\n\n### Manipulation\n\nThere are some additional methods added on top of the `DataSource` methods:\n\n```kotlin\nval dataSource: SelectableDataSource\u003cAny\u003e = // ...\n\n// Index operations\ndataSource.selectAt(1)\ndataSource.deselectAt(1)\ndataSource.toggleSelectionAt(1)\nval selected: Boolean = dataSource.isSelectedAt(1)\n\n// Item operations, uses index operations under the hood\nval item: Any = // ...\ndataSource.select(item)\ndataSource.deselect(item)\ndataSource.toggleSelection(item)\nval selected: Boolean = dataSource.isSelected(item)\n\n// Mass operations\ndataSource.selectAll()\ndataSource.deselectAll()\n\n// Misc operations\nval count: Int = dataSource.getSelectionCount()\nval hasSelection: Boolean = dataSource.hasSelection()\n\n// Set a callback invoked when something is selected or deselected\ndataSource.onSelectionChange { dataSource -\u003e }\n```\n\n### Use in Binding\n\nDuring binding of your items, you can access selection states *even if you don't have a direct \nreference to your `DataSource`.*\n\nIn `onBind` blocks, this is done with extensions in `ViewHolder` which provide functions to check \nselection state and select/deselect the current item that is being bound. \n\nIn `onClick` and `onLongClick` blocks, this is done using a type that is passed as `this` which provides the same set \nof functions.\n\n```kotlin\nrecyclerView.setup {\n    withEmptyView(emptyView)\n    withDataSource(dataSource)\n    withItem\u003cMyListItem, MyViewHolder\u003e(R.layout.my_list_item) {\n      onBind(::MyViewHolder) { index, item -\u003e\n          // Selection-related methods that can be used here:\n          isSelected()\n          select()\n          deselect()\n          toggleSelection()\n          hasSelection()\n      }\n      onClick { index -\u003e\n          // Selection-related methods that can be used here:\n          isSelected()\n          select()\n          deselect()\n          toggleSelection()\n          hasSelection()\n      }\n      onChildViewClick(MyViewHolder::someView) { index, view -\u003e\n          // The same methods used in onClick can be used here as well\n      }\n      onLongClick { index -\u003e\n          // The same methods used in onClick can be used here as well\n      }\n    }\n}  \n```\n\n---\n\n## Stable IDs\n\nStable IDs are an optimization hint for `RecyclerView`. When using stable IDs, you're telling \nthe view that each ViewHolder ID is unique and will not change. In Recyclical, to can use stable IDs\nby having *all* of your items provide a unique ID for themselves.\n\n```kotlin\ndata class AnItemWithAnId(\n  val id: Int,\n  val name: String\n)\n\nrecyclerView.setup {\n  withDataSource(dataSource)\n  withItem\u003cAnItemWithAnId, MyViewHolder\u003e(R.layout.my_item_layout) {\n     onBind(::MyViewHolder) { index, item -\u003e ... }\n     // The key is this, which says the `id` field of your item represents a unique ID.\n     hasStableIds { it.id }\n  }\n}\n```\n\nIf you have more than one item that your RecyclerView can hold, *all* need to define `hasStableIds`.\n\n---\n\n# Swipe\n\nThe swipe module provides extensions to setup swipe actions, like swipe to delete.\n\n[ ![Swipe](https://img.shields.io/maven-central/v/com.afollestad/recyclical-swipe?style=flat\u0026label=Swipe) ](https://repo1.maven.org/maven2/com/afollestad/recyclical-swipe)\n\n## Gradle Dependency\n\nAdd this to your module's `build.gradle` file:\n\n```gradle\ndependencies {\n\n  implementation 'com.afollestad:recyclical-swipe:1.0.1'\n}\n```\n\n## The Basics\n\n\u003cimg src=\"https://raw.githubusercontent.com/afollestad/recyclical/master/art/swipenormal.gif\" width=\"200\" /\u003e\n\nThis example below sets up swipe to delete, so that it works if you swipe either right or left.\nA delete icon and delete text would be shown over a red gutter. *The callback returning true means \nthat the item should be removed from the `DataSource` when the action triggers.*\n\n```kotlin\nlist.setup {\n  ...\n  withSwipeAction(LEFT, RIGHT) {\n    icon(R.drawable.ic_delete)\n    text(R.string.delete)\n    color(R.color.md_red)\n    callback { index, item -\u003e true }\n  }\n}\n```\n\nYou can target specific item types with `withSwipeActionOn`, too:\n\n```kotlin\nwithSwipeActionOn\u003cMyItem\u003e(LEFT, RIGHT) {\n  icon(R.drawable.ic_delete)\n  text(R.string.delete)\n  color(R.color.md_red)\n  callback { index, item -\u003e true }\n}\n```\n\nWith `withSwipeActionOn`, `item` in the callback is a `MyItem` instead of `Any` as well.\n\n## Customization\n\nAs you saw above, you can use icons, text, and background colors easily. There are more details \nyou can customize about your swipe actions, mainly around text:\n\n```kotlin\nlist.setup {\n  ...\n  withSwipeAction(LEFT, RIGHT) {\n    text(\n      res = R.string.delete,\n      color = R.color.black,\n      size = R.dimen.small_text_size,\n      typefaceRes = R.font.roboto_mono\n    )\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafollestad%2Frecyclical","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fafollestad%2Frecyclical","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafollestad%2Frecyclical/lists"}