Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/afollestad/recyclical

🚀 An easy-to-use, extensible Kotlin DSL for setting up and manipulating RecyclerViews.
https://github.com/afollestad/recyclical

List: recyclical

android androidx dsl kotlin list-selection lists recyclerview user-interface

Last synced: 13 days ago
JSON representation

🚀 An easy-to-use, extensible Kotlin DSL for setting up and manipulating RecyclerViews.

Awesome Lists containing this project

README

        

# Recyclical

*recyclical*: an easy-to-use, extensible Kotlin DSL for setting up and manipulating RecyclerViews.

[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bdc552fb3832423986a296a47b9ddef0)](https://www.codacy.com/app/drummeraidan_50/recyclical?utm_source=github.com&utm_medium=referral&utm_content=afollestad/recyclical&utm_campaign=Badge_Grade)
[![Android CI](https://github.com/afollestad/recyclical/workflows/Android%20CI/badge.svg)](https://github.com/afollestad/recyclical/actions?query=workflow%3A%22Android+CI%22)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html)

---

This 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`.

# Table of Contents

## Core

1. [Gradle Dependency](#gradle-dependency)
2. [The Basics](#the-basics)
3. [More Options](#more-options)
4. [Child View Clicks](#child-view-clicks)
5. [Multiple Item Types](#multiple-item-types)
6. [DataSource](#datasource)
1. [Construction](#construction)
2. [Manipulation](#manipulation)
4. [Diffing](#diffing)
7. [SelectableDataSource](#selectabledatasource)
1. [Construction](#construction-1)
2. [Manipulation](#manipulation-1)
3. [Use in Binding](#use-in-binding)
8. [Stable IDs](#stable-ids)

## Swipe

1. [Gradle Dependency](#gradle-dependency-1)
2. [The Basics](#the-basics-1)
3. [Long Swipes](#long-swipes)
4. [Customization](#customization)

---

# Core

[ ![Core](https://img.shields.io/maven-central/v/com.afollestad/recyclical?style=flat&label=Core) ](https://repo1.maven.org/maven2/com/afollestad/recyclical)

## Gradle Dependency

Add this to your module's `build.gradle` file:

```gradle
dependencies {

implementation 'com.afollestad:recyclical:1.1.1'
}
```

---

## The Basics

**First**, declare an Item class:

```kotlin
data class Person(
var name: String,
var arg: Int
)
```

**Second**, a layout and a View Holder:

```xml




```

```kotlin
class PersonViewHolder(itemView: View) : ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.text_name)
val age: TextView = itemView.findViewById(R.id.text_age)
}
```

**Finally**, you can begin using the DSL API:

```kotlin
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// dataSourceTypedOf(...) here creates a DataSource
val dataSource = dataSourceTypedOf(
Person("Aidan", 24),
Person("Nina", 24)
)

// setup{} is an extension method on RecyclerView
recyclerView.setup {
withDataSource(dataSource)
withItem(R.layout.person_item_layout) {
onBind(::PersonViewHolder) { index, item ->
// PersonViewHolder is `this` here
name.text = item.name
age.text = "${item.age}"
}
onClick { index ->
// item is a `val` in `this` here
toast("Clicked $index: ${item.name}")
}
onLongClick { index ->
// item is a `val` in `this` here
toast("Long clicked $index: ${item.name}")
}
}
}
}
}
```

---

## More Options

There are other things you can give to the setup extension:

```kotlin
recyclerView.setup {
// Custom layout manager, rather than the default which is a vertical LinearLayoutManager
withLayoutManager(GridLayoutManager(context, 2))
// Assigns a view that is made visible when the data source has no content, else is hidden (gone)
withEmptyView(view)
// Global click listener for any item type. Individual item click listeners are called first.
withClickListener { index, item -> }
// Global long click listener for any item type. Individual item long click listeners are called first.
withLongClickListener { index, item -> }
// Add an animation used to animate the group's children after the first layout.
withLayoutAnimation(R.anim.your_anim, durationRes = android.R.integer.config_animShortTime)
}
```

---

## Child View Clicks

There are many cases in which you'd want to get callbacks for a child view in your list items
getting clicked, such as the sender icon in a list of emails.

```kotlin
class EmailViewHolder(itemView: View) : ViewHolder(itemView) {
val icon = itemView.findViewById(R.id.icon)
}

recyclerView.setup {
withItem(R.layout.email_item_layout) {
...
onChildViewClick(EmailViewHolder::icon) { index, view ->
// `this` includes `item` along with selection-related methods discussed below in SelectableDataSource
// `view` argument here is automatically an `ImageView`
}
}
}
```

---

## Multiple Item Types

You can mix different types of items - but you need to specify view holders and layouts for them too:

```kotlin
// dataSourceOf(...) without "typed" creates a DataSource
val dataSource = dataSourceOf(
Car(2012, "Volkswagen GTI"),
Motorcycle(2018, "Triumph", "Thruxton R"),
Person("Aidan", 24)
)

recyclerView.setup {
withDataSource(dataSource)
withItem(R.layout.person_item_layout) {
onBind(::PersonViewHolder) { index, item ->
name.text = item.name
age.text = "${item.age}"
}
}
withItem(R.layout.motorcycle_item_layout) {
onBind(::MotorcycleViewHolder) { index, item ->
year.text = "${item.year}"
make.text = item.make
model.text = item.model
}
}
withItem(R.layout.car_item_layout) {
onBind(::CarViewHolder) { index, item ->
year.text = "${item.year}"
name.text = item.name
}
}
}
```

---

## DataSource

`DataSource` is an interface which provides data and allows manipulation of the data, to display in a RecyclerView.
Being an interface means you make your own implementations of it, you can mock it in tests, you could even provide it
via Dagger to a presenter and manipulate the RecyclerView outside of your UI layer.

### Construction

The included implementation of data source operates on a List of objects (of any type).

```kotlin
// Empty by default, but can still add, insert, etc.
val dataSource: DataSource = emptyDataSource()
val dataSourceTyped: DataSource = emptyDataSourceTyped()

// Initial data set of items from a vararg list
val dataSource: DataSource = dataSourceOf(item1, item2)
val dataSourceTyped: DataSource = dataSourceTypedOf(item1, item2)

// Initial data set of items from an existing list
// Could also use dataSourceTypedOf(...)
val items = listOf(item1, item2)
val dataSource: DataSource = dataSourceOf(items)
val dataSourceTyped: DataSource = dataSourceTypedOf(items)

```

### Manipulation

```kotlin
val dataSource: DataSource = // ...

// getters
val item: ItemType = dataSource[5]
val contains: Boolean = dataSource.contains(item)
val size: Int = dataSource.size()
val isEmpty: Boolean = dataSource.isEmpty()
val isNotEmpty: Boolean = dataSource.isNotEmpty()
val firstIndex: Int = dataSource.indexOfFirst { }
val lastIndex: Int = dataSource.indexOfLast { }

// mutation
val person = Person("Aidan", 24)
dataSource.add(person)
dataSource.set(listOf(person))
dataSource.insert(1, person)
dataSource.removeAt(1)
dataSource.remove(person)
dataSource.swap(1, 4)
dataSource.move(1, 4)
dataSource.clear()

// iteration
for (item in dataSource) { }
dataSource.forEach { } // emits all items
dataSource.forEachOf { } // only emits items that are a Person

// operators
val item: Any = dataSource[5] // get(5)
val contains: Boolean = item in dataSource // contains(item)
dataSource += person // add(person)
dataSource -= person // remove(person)
```

### Diffing

When performing a `set` on the data set, you can opt to use diff utils:

```kotlin
dataSource.set(
newItems = newItems,
areTheSame = ::areItemsTheSame,
areContentsTheSame = ::areItemContentsTheSame
)

// Return true if items represent the same entity, e.g. by ID or name
private fun areItemsTheSame(left: Any, right: Any): Boolean {
return when (left) {
is Person -> {
right is Person && right.name == left.name
}
else -> false
}
}

// Return true if all contents in the items are equal
private fun areItemContentsTheSame(left: Any, right: Any): Boolean {
return when (left) {
is Person -> {
right is Person &&
right.name == left.name &&
right.age == left.age
}
else -> false
}
}
```

This will automatically coordinate notifying of adds, moves, and insertions so that
update of the data set is pretty and animated by the RecyclerView.

---

## SelectableDataSource

A `SelectableDataSource` is built on top of a regular [DataSource]. It provides additional APIs
to manage the selection state of items in your list.

### Construction

Construction methods for `SelectableDataSource` are the same as the `DataSource` ones, they just
include `selectable` in their names.

```kotlin
// Empty by default, but can still add, insert, etc.
// Could also use emptySelectableDataSourceTyped()
val dataSource: SelectableDataSource = emptySelectableDataSource()
val dataSourceTyped: SelectableDataSource = emptySelectableDataSourceTyped()

// Initial data set of items from a vararg list
// Could also use selectableDataSourceTypedOf(...)
val dataSource: SelectableDataSource = selectableDataSourceOf(item1, item2)
val dataSourceTyped: SelectableDataSource = selectableDataSourceTypedOf(item1, item2)

// Initial data set of items from an existing list
// Could also use selectableDataSourceTypedOf(...)
val items = listOf(item1, item2)
val dataSource: SelectableDataSource = selectableDataSourceOf(items)
val dataSourceTyped: SelectableDataSource = selectableDataSourceTypedOf(items)
```

### Manipulation

There are some additional methods added on top of the `DataSource` methods:

```kotlin
val dataSource: SelectableDataSource = // ...

// Index operations
dataSource.selectAt(1)
dataSource.deselectAt(1)
dataSource.toggleSelectionAt(1)
val selected: Boolean = dataSource.isSelectedAt(1)

// Item operations, uses index operations under the hood
val item: Any = // ...
dataSource.select(item)
dataSource.deselect(item)
dataSource.toggleSelection(item)
val selected: Boolean = dataSource.isSelected(item)

// Mass operations
dataSource.selectAll()
dataSource.deselectAll()

// Misc operations
val count: Int = dataSource.getSelectionCount()
val hasSelection: Boolean = dataSource.hasSelection()

// Set a callback invoked when something is selected or deselected
dataSource.onSelectionChange { dataSource -> }
```

### Use in Binding

During binding of your items, you can access selection states *even if you don't have a direct
reference to your `DataSource`.*

In `onBind` blocks, this is done with extensions in `ViewHolder` which provide functions to check
selection state and select/deselect the current item that is being bound.

In `onClick` and `onLongClick` blocks, this is done using a type that is passed as `this` which provides the same set
of functions.

```kotlin
recyclerView.setup {
withEmptyView(emptyView)
withDataSource(dataSource)
withItem(R.layout.my_list_item) {
onBind(::MyViewHolder) { index, item ->
// Selection-related methods that can be used here:
isSelected()
select()
deselect()
toggleSelection()
hasSelection()
}
onClick { index ->
// Selection-related methods that can be used here:
isSelected()
select()
deselect()
toggleSelection()
hasSelection()
}
onChildViewClick(MyViewHolder::someView) { index, view ->
// The same methods used in onClick can be used here as well
}
onLongClick { index ->
// The same methods used in onClick can be used here as well
}
}
}
```

---

## Stable IDs

Stable IDs are an optimization hint for `RecyclerView`. When using stable IDs, you're telling
the view that each ViewHolder ID is unique and will not change. In Recyclical, to can use stable IDs
by having *all* of your items provide a unique ID for themselves.

```kotlin
data class AnItemWithAnId(
val id: Int,
val name: String
)

recyclerView.setup {
withDataSource(dataSource)
withItem(R.layout.my_item_layout) {
onBind(::MyViewHolder) { index, item -> ... }
// The key is this, which says the `id` field of your item represents a unique ID.
hasStableIds { it.id }
}
}
```

If you have more than one item that your RecyclerView can hold, *all* need to define `hasStableIds`.

---

# Swipe

The swipe module provides extensions to setup swipe actions, like swipe to delete.

[ ![Swipe](https://img.shields.io/maven-central/v/com.afollestad/recyclical-swipe?style=flat&label=Swipe) ](https://repo1.maven.org/maven2/com/afollestad/recyclical-swipe)

## Gradle Dependency

Add this to your module's `build.gradle` file:

```gradle
dependencies {

implementation 'com.afollestad:recyclical-swipe:1.0.1'
}
```

## The Basics

This example below sets up swipe to delete, so that it works if you swipe either right or left.
A delete icon and delete text would be shown over a red gutter. *The callback returning true means
that the item should be removed from the `DataSource` when the action triggers.*

```kotlin
list.setup {
...
withSwipeAction(LEFT, RIGHT) {
icon(R.drawable.ic_delete)
text(R.string.delete)
color(R.color.md_red)
callback { index, item -> true }
}
}
```

You can target specific item types with `withSwipeActionOn`, too:

```kotlin
withSwipeActionOn(LEFT, RIGHT) {
icon(R.drawable.ic_delete)
text(R.string.delete)
color(R.color.md_red)
callback { index, item -> true }
}
```

With `withSwipeActionOn`, `item` in the callback is a `MyItem` instead of `Any` as well.

## Customization

As you saw above, you can use icons, text, and background colors easily. There are more details
you can customize about your swipe actions, mainly around text:

```kotlin
list.setup {
...
withSwipeAction(LEFT, RIGHT) {
text(
res = R.string.delete,
color = R.color.black,
size = R.dimen.small_text_size,
typefaceRes = R.font.roboto_mono
)
}
}
```