Ecosyste.ms: Awesome

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

Awesome Lists | Featured Topics | Projects

https://github.com/detmir/recycli

Recycli is a Kotlin Android library that simplifies building complex multiple view types screens in a RecyclerView
https://github.com/detmir/recycli

diffutil diffutil-recyclerview recycler-view recyclerview recyclerview-adapter recyclerviewadapter

Last synced: about 2 months ago
JSON representation

Recycli is a Kotlin Android library that simplifies building complex multiple view types screens in a RecyclerView

Awesome Lists containing this project

README

        

![recycli_logo](https://user-images.githubusercontent.com/1109620/115422301-7405b780-a205-11eb-9372-1411ff17168d.png)

![GitHub](https://img.shields.io/github/license/detmir/recycli?logo=heartex) ![GitHub release](https://img.shields.io/github/v/release/detmir/recycli?include_prereleases)

Recycli is a Kotlin library for Android RecyclerView that simplifies the creation of multiple view types lists. Featuring DiffUtils inside, annotation-based adapter generator and MVI pattern as philosophy, it is both a simple and powerful tool for rapid development of RecyclerView-based screens.

![ezgif-6-e9d7bd416187](https://user-images.githubusercontent.com/1109620/115579256-a7f8df80-a2ce-11eb-9bc2-ac79d3905b89.gif)

# Table of Contents
[Installation](#installation)
[First steps](#first_steps)
[Use Views or ViewHolders](#view_holders)
[Reaction on clicks and state changes](#clicks_and_state)
[Sealed classes as states](#sealed)
[Sealed classes and binding functions](#sealed_binfing)
[One item state and several views](#one_several)
[Horizontal sub lists](#horizontal)
[Multi-module applications](#multi_module)
[Endless scrolling lists](#infinity)
[Paging 3](#paging3)
[Sticky Headers](#sticky_headers)
[License](#license)

# Installation
1. Add Maven Central to you repositories in the `build.gradle` file at the project or module level:

```gradle
allprojects {
repositories {
mavenCentral()
}
}
```
2. Add KSP plugin to plugins section of your `build.gradle` at the project level. Select KSP version that matches your Kotlin version (`1.8.22` is the version of Kotlin the plugin matches). Minimum supported version of Kotlin is `1.8.xx`. If your version is lower, please use 1.9.0 version of Recycli, that works with any Kotlin version using KAPT instead of KSP. See the documentation [https://github.com/detmir/recycli/tree/kapt](https://github.com/detmir/recycli/tree/kapt)
```gradle
plugins {
id 'com.google.devtools.ksp' version '1.8.22-1.0.11'
}
```
3. Add KSP plugin and Recycli dependencies to your 'build.gradle' at the module level:
```gradle
apply plugin: 'com.google.devtools.ksp'

dependencies {
implementation 'com.detmir.recycli:adapters:2.2.0'
compileOnly 'com.detmir.recycli:annotations:2.2.0'
ksp 'com.detmir.recycli:processors:2.2.0'
}

```

# First steps
1. Create Kotlin data classes that are annotated with `@RecyclerItemState` and are extending `RecyclerItem`. A unique (for this adapter) string `id` must be provided. Those classes describe recycler items states. Let's create two data classes - Header and User items:

```java
@RecyclerItemState
data class HeaderItem(
val id: String,
val title: String
) : RecyclerItem {
override fun provideId() = id
}
```

```java
@RecyclerItemState
data class UserItem(
val id: String,
val firstName: String
) : RecyclerItem {
override fun provideId() = id
}
```

2. Add two view classes `HeaderItemView` and `UserItemView` that extend any `View` or `ViewGroup` container. Annotate these classes with `@RecyclerItemView` annotation. Also, add a method with recycler item state as a parameter and annotate it with `@RecyclerItemStateBinder`.

```java
@RecyclerItemView
class HeaderItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

private val title: TextView

init {
LayoutInflater.from(context).inflate(R.layout.header_view, this)
title = findViewById(R.id.header_view_title)
}

@RecyclerItemStateBinder
fun bindState(headerItem: HeaderItem) {
title.text = headerItem.title
}
}
```

```java
@RecyclerItemView
class UserItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

private val firstName: TextView

init {
LayoutInflater.from(context).inflate(R.layout.user_view, this)
firstName = findViewById(R.id.user_view_first_name)
}

@RecyclerItemStateBinder
fun bindState(userItem: UserItem) {
firstName.text = userItem.firstName
}
}
```

Those views will be used in `onCreateViewHolder` functions in `RecyclerView.Adapter` for corresponding states. `bindState` will be called when `onBindViewHolder` called in the adapter.

3. Create `RecyclerView` and bind the list of `RecyclerItems` to it with `bindState` method.
The Recycler Adpater class is generated by Recycli lib under the hood using the annotations mentioned earlier.

```java
class DemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val recyclerView = findViewById(R.id.activity_case_0100_recycler)
recyclerView.layoutManager = LinearLayoutManager(this)

recyclerView.bindState(
listOf(
HeaderItem(
id = "HEADER_USERS",
title = "Users"
),
UserItem(
id = "USER_ANDREW",
firstName = "Andrew",
online = true
),
UserItem(
id = "USER_MAX",
firstName = "Max",
online = true
)
)
)
}
}
```

The `RecyclerView` will display:

![Screenshot_20210423-151457_KKppt3](https://user-images.githubusercontent.com/1109620/115869752-03e67400-a447-11eb-9d63-0c78e98bb4f7.png)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0100SimpleActivity.kt)

# Use Views or ViewHolders

In the example earlier, we used classes that extend `ViewGroup` or `View` to provide `RecyclerView` with the corresponding view. If you prefer to inflate views directly in `RecyclerView.ViewHolder`, you can do it with `@RecyclerItemViewHolder` and `@RecyclerItemViewHolderCreator` annotations. Note that `@RecyclerItemViewHolderCreator` must be a function located in the companion class of `ViewHolder`.

See the full example below:

* Recycler item state:

```java
@RecyclerItemState
data class ServerItem(
val id: String,
val serverAddress: String
) : RecyclerItem {
override fun provideId() = id
}
```

* View holder that can bind `ServerItem` state:

```java
@RecyclerItemViewHolder
class ServerItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val serverAddress: TextView = view.findViewById(R.id.server_item_title)

@RecyclerItemStateBinder
fun bindState(serverItem: ServerItem) {
serverAddress.text = serverItem.serverAddress
}

companion object {
@RecyclerItemViewHolderCreator
fun provideViewHolder(context: Context): ServerItemViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.server_item_view, null)
return ServerItemViewHolder(view)
}
}
}
```

* Bind items to `RecyclerView`:

```java
class Case0101SimpleVHActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val recyclerView = findViewById(R.id.activity_case_0101_recycler)
recyclerView.layoutManager = LinearLayoutManager(this)

recyclerView.bindState(
listOf(
HeaderItem(
id = "HEADER_SERVERS",
title = "Servers"
),
ServerItem(
id = "SERVER1",
serverAddress = "124.45.22.12"
),
ServerItem(
id = "SERVER2",
serverAddress = "90.0.0.28"
)
)
)
}
}
```

The result:

![Screenshot_20210423-150530_KKppt3](https://user-images.githubusercontent.com/1109620/115868740-938b2300-a445-11eb-850e-8404e52dab90.png)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0101SimpleVHActivity.kt)

# Reaction on clicks and state changes

Click reaction is handled in MVI manner. Recycler item provides the intent via its state function invocation. ViewModel handles the intent, recalculates the state and binds it to the adapter.

1. Provide the recycler item state with click reaction functions:

```java
@RecyclerItemState
data class UserItem(
val id: String,
val firstName: String,
val onCardClick: (String) -> Unit,
val onMoveToOnline: (String) -> Unit,
val onMoveToOffline: (String) -> Unit
) : RecyclerItem {
override fun provideId() = id
}
```

2. Add on-click listeners to the view:

```java
@RecyclerItemView
class UserItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

private lateinit var userItem: UserItem

init {
...
toOnlineButton.setOnClickListener {
userItem.onMoveToOnline.invoke(userItem.firstName)
}

toOfflineButton.setOnClickListener {
userItem.onMoveToOffline.invoke(userItem.firstName)
}

holder.setOnClickListener {
userItem.onCardClick.invoke(userItem.firstName)
}
}

@RecyclerItemStateBinder
fun bindState(userItem: UserItem) {
this.userItem = userItem
firstName.text = userItem.firstName
}
}
```

3. In your ViewModel you can handle the clicks, recreate the state if needed and bind it to your recyclerView using `bindState`:

```java
private val onlineUserNames = mutableListOf("James","Mary","Robert","Patricia")
private val offlineUserNames = mutableListOf("Michael","Linda","William","Elizabeth","David")

private fun updateRecycler() {
val recyclerItems = mutableListOf()

recyclerItems.add(
HeaderItem(
id = "HEADER_ONLINE_OPERATORS",
title = "Online operators ${onlineUserNames.size}"
)
)

onlineUserNames.forEach { name ->
recyclerItems.add(
UserItem(
id = name,
firstName = name,
online = true,
onCardClick = ::cardClicked,
onMoveToOffline = ::moveToOffline
)
)
}

recyclerItems.add(
HeaderItem(
id = "HEADER_OFFLINE_OPERATORS",
title = "Offline operators ${offlineUserNames.size}"
)
)

offlineUserNames.forEach {
recyclerItems.add(
UserItem(
id = it,
firstName = it,
online = false,
onCardClick = ::cardClicked,
onMoveToOnline = ::moveToOnline
)
)
}

recyclerView.bindState(recyclerItems)
}

private fun cardClicked(name: String) {
Toast.makeText(this, name, Toast.LENGTH_SHORT).show()
}

private fun moveToOffline(name: String) {
onlineUserNames.remove(name)
offlineUserNames.add(0, name)
updateRecycler()
}

private fun moveToOnline(name: String) {
offlineUserNames.remove(name)
onlineUserNames.add(name)
updateRecycler()
}
```

Note that we have implemented all the logic inside Activity for simplification purposes.

The result:

![ezgif-6-a9ba26ee168f](https://user-images.githubusercontent.com/1109620/115968128-e0065980-a53e-11eb-9b9e-e0049272edc3.gif)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0200ClickAndStateActivity.kt)

# Sealed classes as states

Using sealed classes as UI states is a common thing. You can create sealed class state items and bind them easily.

1. Create a sealed class:

```java
@RecyclerItemState
sealed class ProjectItem : RecyclerItem {
abstract val id: String
abstract val title: String

data class Failed(
override val id: String,
override val title: String,
val why: String
) : ProjectItem()

data class New(
override val id: String,
override val title: String
) : ProjectItem()

sealed class Done: ProjectItem() {
data class BeforeDeadline(
override val id: String,
override val title: String
) : Done()

data class AfterDeadline(
override val id: String,
override val title: String,
val why: String
) : Done()
}

override fun provideId() = id
}
```

2. Use Kotlin `when` to handle different sealed class states:

```java
@RecyclerItemView
class ProjectItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

@RecyclerItemStateBinder
fun bindState(projectItem: ProjectItem) {
projectTitle.text = projectItem.title
when (projectItem) {
is ProjectItem.Failed -> projectDescription.text = "Failed"
is ProjectItem.New -> projectDescription.text = "New"
is ProjectItem.Done.AfterDeadline -> projectDescription.text = "After deadline"
is ProjectItem.Done.BeforeDeadline -> projectDescription.text = "Before deadline"
}
}
}
```

3. Create and bind the recycler state:

```java
recyclerView.bindState(
listOf(
ProjectItem.Failed(
id = "FAILED",
title = "Failed project",
why = ""
),
ProjectItem.New(
id = "NEW",
title = "New project"
),
ProjectItem.Done.BeforeDeadline(
id = "BEFORE_DEAD_LINE",
title = "Done before deadline project"
),
ProjectItem.Done.AfterDeadline(
id = "AFTER_DEAD_LINE",
title = "Done after deadline project",
why = ""
)
)
)
```

The result:

![Screenshot_20210423-170301_KKppt3](https://user-images.githubusercontent.com/1109620/115882923-e967c700-a455-11eb-8bcc-3990b9a740fb.png)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0300SealedActivity.kt)

# Sealed classes and binding functions

You can create binding functions for every subclass of a sealed state (or even for sealed sub classes of a sealed class).

Sealed class recycler item state:

```java
@RecyclerItemState
sealed class PipeLineItem : RecyclerItem {
data class Input(
val id: String,
val from: String
) : PipeLineItem() {
override fun provideId() = id
}

data class Output(
val id: String,
val to: String
) : PipeLineItem() {
override fun provideId() = id
}
}
```

```java
@RecyclerItemView
class PipeLineItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
...
@RecyclerItemStateBinder
fun bindState(input: PipeLineItem.Input) {
destination.text = input.from
}

@RecyclerItemStateBinder
fun bindState(output: PipeLineItem.Output) {
destination.text = output.to
}
}
```

See:

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0301SealedSeveralBindsActivity.kt)

# One item state and several views

Sometimes one needs several view variants for one recycler item state class. You can define which view to use by overriding the `withView()` method of `RecyclerItem`:

```java
@RecyclerItemState
data class CloudItem(
val id: String,
val serverName: String,
val intoView: Class
) : RecyclerItem {
override fun provideId() = id
override fun withView() = intoView
}
```

1. Create several views or view holders that can bind CloudItem:

```java
@RecyclerItemView
class CloudAzureItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
....
@RecyclerItemStateBinder
fun bindState(cloudItem: CloudItem) {
name.text = cloudItem.serverName
}
}
```

```java
@RecyclerItemView
class CloudAmazonItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
....
@RecyclerItemStateBinder
fun bindState(cloudItem: CloudItem) {
name.text = cloudItem.serverName
}
}
```

```java
@RecyclerItemViewHolder
class DigitalOceanViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@RecyclerItemStateBinder
fun bindState(cloudItem: CloudItem) {
name.text = cloudItem.serverName
}

companion object {
@RecyclerItemViewHolderCreator
fun provideViewHolder(context: Context): DigitalOceanViewHolder {
return DigitalOceanViewHolder(LayoutInflater.from(context).inflate(R.layout.cloud_digital_ocean_item_view, null))
}
}
}
```

2. Then, fill the recyclerView with items:

```java
class DemoActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
....
recyclerView.bindState(
listOf(
CloudItem(
id = "GOOGLE",
serverName = "Google server",
intoView = CloudGoogleItemView::class.java
),
CloudItem(
id = "AMAZON",
serverName = "Amazon server",
intoView = CloudAmazonItemView::class.java
),
CloudItem(
id = "AZURE",
serverName = "Azure server",
intoView = CloudAzureItemView::class.java
),
CloudItem(
id = "DIGITAL_OCEAN",
serverName = "Digital ocean server",
intoView = DigitalOceanViewHolder::class.java
)
)
)
}
}
```

The result:

![Screenshot_20210424-214134_KKppt3](https://user-images.githubusercontent.com/1109620/115969528-02e83c00-a546-11eb-8787-43918cf84a69.png)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0400IntoViewActivity.kt)

# Horizontal sub lists

It's common to have horizontal scrolling lists inside the vertical scrolling container, and recycli supports this feature.

1. Create a container state and view for horizontal list. This is just another list of items, recycler with horizontal layout manager and adapter:

```java
@RecyclerItemState
data class SimpleContainerItem(
val id: String,
val recyclerState: List
): RecyclerItem {
override fun provideId(): String {
return id
}
}
```

```java
@RecyclerItemView
class SimpleContainerItemView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val recycler: RecyclerView
private val recyclerAdapter: RecyclerAdapter

init {
val view =
LayoutInflater.from(context).inflate(R.layout.simple_recycler_conteiner_view, this, true)
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)

recyclerAdapter = RecyclerAdapter()
recycler = view.findViewById(R.id.simple_recycler_container_recycler)

recycler.run {
isNestedScrollingEnabled = false
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
adapter = recyclerAdapter
}
}

@RecyclerItemStateBinder
fun bindState(state: SimpleContainerItem) {
recyclerAdapter.bindState(state.recyclerState)
}
}
```

2. Now, populate recycler items and sublist items in a usual way:

```java
class DemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerAdapter.bindState(
listOf(
HeaderItem(
id = "HEADER_SUB_TASKS",
title = "Subtasks"
),
SimpleContainerItem(
id = "SUB_TASKS_CONTAINER",
recyclerState = (0..100).map {
SubTaskItem(
id = "SUB_TASK_$it",
title = "Sub task $it",
description = "It is a long established ..."
)
}
),
BigTaskItem(
id = "TASK",
title = "The second task title",
description = "It is a long established ..."
)
)
)
}
}
```

The result:

![ezgif-2-9cf09cc92026](https://user-images.githubusercontent.com/1109620/115970440-0500c980-a54b-11eb-857f-9c955b7cb371.gif)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0500HorizontalActivity.kt)

# Multi-module applications

If your app has several modules and your recycler item states and views are located in different modules,
Recycli will manage needed adapters for you under the hood

See:

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0600InfinityActivity.kt)

# Infinite scrolling lists
One of the main features of Recycli - support for infinite scroll lists. It handles paging loading callbacks, displain bottom progress bars and errors.
To create an infinite scroll list, just pass `RecyclerAdapter.Callbacks` to the recyclerView, this will switch it to infinite scroll. `BottomLoading` is optional. It is responsible for displaying bottom progress bar, error page loading and for dummy item that provides some extra space for better load position detection:

```java
recyclerView.setInfinityCallbacks(this)
recyclerView.setBottomLoading(BottomLoading())
```

1. You need to provide the `loadRange` function to implement infinite callback interface `RecyclerAdapter.Callbacks`.
Adapter does not initiate loading of the first page, so we have to call `loadRange(0)` to initiate loading. All the later pages will loaded by the adapter when you scroll the recycler.

2. When the adapter invokes `loadRange`, you need to bind `InfinityState` with `requestState = InfinityState.Request.LOADING` first: the adapter will understand that loading process has started, and will stop calling `loadRange`. You also need to pass current items, loading page number and provide boolean `endReached` to indicate there are no more data:

```java
recyclerView.bindState(
InfinityState(
requestState = InfinityState.Request.LOADING,
items = items,
page = curPage,
endReached = curPage == 10
)
)
```

3. Once you load data, add it to your items and pass it to adapter with the IDLE state:

```java
if (curPage == 0) items.clear()
items.addAll(it)

recyclerView.bindState(
InfinityState(
requestState = InfinityState.Request.IDLE,
items = items,
page = curPage,
endReached = curPage == 10
)
)
```

4. If you encounter an error while loading data, bind `InfinityState` with `InfinityState.Request.ERROR`. Consider the example below:

* We load 10 pages (20 items per page).
* On page 4, we emulate error and bind error state to show error button appears at the bottom.
* When page 10 is loaded, we set `endReached` to true and adapter stops asking for more data.
* We use RX to emulate loading process with 2 seconds data loading delay.

```java
class DemoActivity : AppCompatActivity(), RecyclerAdapter.Callbacks {

private val items = mutableListOf()
private val PAGE_SIZE = 20

override fun onCreate(savedInstanceState: Bundle?) {
recyclerView.setInfinityCallbacks(this)
recyclerView.setBottomLoading(BottomLoading())
loadRange(0)
}

override fun loadRange(curPage: Int) {
val delay = if (curPage == 0) 0L else 2000L
Single.timer(delay, TimeUnit.MILLISECONDS)
.flatMap {
Single.just((curPage * PAGE_SIZE until (curPage * PAGE_SIZE + PAGE_SIZE)).map {
UserItem(
id = "$it",
firstName = "John $it",
online = it < 5
)
})
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map {
if (curPage == 4 && !infiniteItemsErrorThrown) {
infiniteItemsErrorThrown = true
throw Exception("error")
}
it
}
.doOnSubscribe {
recyclerAdapterrecyclerView.bindState(
InfinityState(
requestState = InfinityState.Request.LOADING,
items = items,
page = curPage,
endReached = curPage == 10
)
)
}
.doOnError {
recyclerView.bindState(
InfinityState(
requestState = InfinityState.Request.ERROR,
items = items,
page = curPage,
endReached = curPage == 10
)
)
}
.doOnSuccess {
if (curPage == 0) items.clear()
items.addAll(it)

recyclerView.bindState(
InfinityState(
requestState = InfinityState.Request.IDLE,
items = items,
page = curPage,
endReached = curPage == 10
)
)
}
.subscribe({}, {})
}
}
```

Keep in mind that you need to implement the `RecyclerBottomLoading` interface and pass it to adapter to provide `Dummy`, `Progress`, `Error` and `Button` recycler items states that will be displayed while you scroll. This is optional, but in production apps it is a standart UI you have to implement:

```java
class BottomLoading : RecyclerBottomLoading {

@RecyclerItemState
sealed class State : RecyclerItem {
override fun provideId(): String {
return "bottom"
}
object Dummy : State()
object Progress : State()
data class Error(val reload: () -> Unit) : State()
data class Button(val next: () -> Unit) : State()
}

override fun provideProgress(): RecyclerItem {
return State.Progress
}

override fun provideDummy(): RecyclerItem {
return State.Dummy
}

override fun provideError(reload: () -> Unit): RecyclerItem {
return State.Error(reload)
}

override fun provideButton(next: () -> Unit): RecyclerItem {
return State.Button(next)
}
}
```

```java
@RecyclerItemView
class BottomLoadingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
...
@RecyclerItemStateBinder
fun bindState(state: BottomLoading.State) {
when (state) {
is BottomLoading.State.Progress -> {
buttonError.visibility = View.GONE
progress.visibility = View.VISIBLE
}
is BottomLoading.State.Button -> {
buttonError.visibility = View.GONE
progress.visibility = View.GONE

}
is BottomLoading.State.Dummy -> {
buttonError.visibility = View.GONE
progress.visibility = View.GONE
}

is BottomLoading.State.Error -> {
buttonError.visibility = View.VISIBLE
progress.visibility = View.GONE
}
}
}
}
```

Note that we scroll fast, so you can see loader that displays progress for 2 seconds. In reality users don't scroll that fast and loading process starts when 5 elements are left at the bottom.

The result:

![ezgif-6-5b5d2a89fbdb](https://user-images.githubusercontent.com/1109620/115992211-27d7c000-a5d5-11eb-8753-0b340f54042f.gif)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0600Infinity.kt)

# Jetpack Paging 3

You can use low level Recycli adapter `RecyclerBaseAdapter` to provide ViewHolders and bindings and use Paging 3 library for all needed infinity scroll logic

![paging](https://user-images.githubusercontent.com/1109620/172666638-ddb0377f-c2b8-4dd9-8faf-582b0c4081d5.gif)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0700Paging.kt)

# Sticky headers

You can use standart Item decorator technique to support sticky headers

![sticky](https://user-images.githubusercontent.com/1109620/172666665-016712b7-73c2-4ac1-a4e3-3e73da40875e.gif)

[Demo Activity](https://github.com/detmir/recycli/blob/master/app/src/main/java/com/detmir/kkppt3/Case0800StickyHeaderActivity.kt)

# License

```
Copyright 2021 Detsky Mir Group

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```