Skip to content

Commit

Permalink
Nested RecyclerViews and eased diffing (#10)
Browse files Browse the repository at this point in the history
Improvements:
* added `onHolderCreated` to be able to perform also one-shot bindings when creating a new view holder
* eased diffing implementation
* improved diffing algorithm
* added documentation for carousels
* minor code improvements
  • Loading branch information
gotev authored Mar 16, 2019
1 parent 5dc55f7 commit 8f18fd5
Show file tree
Hide file tree
Showing 42 changed files with 862 additions and 243 deletions.
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ In this way every item of the recycler view has its own set of files, resulting
* [Setup](#setup)
* [Basic usage tutorial](#basicTutorial)
* [Adding different kind of items](#differentItems)
* [Carousels and nested RecyclerViews](#carousels)
* [Empty item](#emptyItem)
* [Filter items (to implement searchBar)](#filterItems)
* [Sort items](#sortItems)
Expand All @@ -34,7 +35,8 @@ In this way every item of the recycler view has its own set of files, resulting
## <a name="setup"></a>Setup
In your gradle dependencies add:
```groovy
implementation 'net.gotev:recycleradapter:2.2.3'
def recyclerAdapterVersion = "2.4.0"
implementation "net.gotev:recycleradapter:$recyclerAdapterVersion"
```

## <a name="basicTutorial"></a>Basic usage tutorial
Expand Down Expand Up @@ -69,14 +71,16 @@ Create your item layout (e.g. `item_example.xml`). For example:
open class ExampleItem(private val context: Context, private val text: String)
: AdapterItem<ExampleItem.Holder>() {

override fun diffingId() = javaClass.name + text

override fun getLayoutId() = R.layout.item_example

override fun bind(holder: ExampleItem.Holder) {
holder.titleField.text = text
}

class Holder(itemView: View, adapter: RecyclerAdapterNotifier)
: RecyclerAdapterViewHolder(itemView, adapter), LayoutContainer {
class Holder(itemView: View)
: RecyclerAdapterViewHolder(itemView), LayoutContainer {

override val containerView: View?
get() = itemView
Expand Down Expand Up @@ -113,6 +117,24 @@ recyclerAdapter.add(TextWithButtonItem("text with button"))

Checkout the example app provided to get a real example in action.

## <a name="carousels"></a>Carousels and nested RecyclerViews
When more complex layouts are needed in a recycler view, you have two choices:

* use a combination of existing layout managers and nest recycler views
* create a custom layout manager

Since the second strategy is really hard to implement and maintain, also due to lack of documentation and concrete working examples without huge memory leaks or crashes, in my experience resorting to the first strategy has always paid off both in terms of simplicity and maintainability.

One of the most common type of nested RecyclerViews are Carousels, like those you can find in Google Play Store. How to achieve that? First of all, include `recycleradapter-extensions` in your gradle:

```groovy
implementation "net.gotev:recycleradapter-extensions:$recyclerAdapterVersion"
```

The concept is really simple. You want to have a whole recycler view inside a single `AdapterItem`. To make things modular and to not reinvent the wheel, you want to be able to use a `RecyclerAdapter` in this nested `RecyclerView`. Please welcome `NestedRecyclerAdapterItem` which eases things for you. Override it to implement your custom nested recycler views. You can find a complete example in [Carousels Activity](https://github.com/gotev/recycler-adapter/blob/master/app/demo/src/main/java/net/gotev/recycleradapterdemo/activities/Carousels.kt) together with a custom [TitledCarousel](https://github.com/gotev/recycler-adapter/blob/master/app/demo/src/main/java/net/gotev/recycleradapterdemo/adapteritems/TitledCarousel.kt)

Since having nested recycler views consumes a lot of memory and you may experience lags in your app, it's recommended to share a single `RecycledViewPool` across all your root and nested `RecyclerView`s. In that way all the `RecyclerView`s will use a single recycled pool like there's only one `RecyclerView`. You can see the performance difference by running the demo app on a low end device and trying Carousels both with pool and without pool.

## <a name="emptyItem"></a>Empty item
It's often useful to display something on the screen when the RecyclerView is empty. To do so, simply implement a new `Item` just as you would do with a normal item in the list, then:

Expand Down Expand Up @@ -260,10 +282,12 @@ One of the things which you may need is to set one or more click listeners to ev
open class ExampleItem(private val context: Context, private val text: String)
: AdapterItem<ExampleItem.Holder>() {

override fun onFilter(searchTerm: String) = text.contains(searchTerm)
override fun diffingId() = javaClass.name + text

override fun getLayoutId() = R.layout.item_example

override fun onFilter(searchTerm: String) = text.contains(searchTerm)

override fun bind(holder: Holder) {
holder.titleField.text = text
holder.subtitleField.text = "subtitle"
Expand All @@ -281,8 +305,8 @@ open class ExampleItem(private val context: Context, private val text: String)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

class Holder(itemView: View, adapter: RecyclerAdapterNotifier)
: RecyclerAdapterViewHolder(itemView, adapter), LayoutContainer {
class Holder(itemView: View)
: RecyclerAdapterViewHolder(itemView), LayoutContainer {

override val containerView: View?
get() = itemView
Expand Down Expand Up @@ -364,12 +388,23 @@ class TextWithButtonItem(private val text: String) : AdapterItem<TextWithButtonI

override fun getLayoutId() = R.layout.item_text_with_button

override fun diffingId() = javaClass.name

override fun hasToBeReplacedBy(newItem: AdapterItem<*>): Boolean {
if (newItem !is TextWithButtonItem) {
return true
}

return text != newItem.text
}

override fun bind(holder: Holder) {
holder.textViewField.text = text
holder.buttonField.isChecked = pressed
}

class Holder(itemView: View, adapter: RecyclerAdapterNotifier) : RecyclerAdapterViewHolder(itemView, adapter), LayoutContainer {
class Holder(itemView: View)
: RecyclerAdapterViewHolder(itemView), LayoutContainer {

override val containerView: View?
get() = itemView
Expand Down
6 changes: 5 additions & 1 deletion app/demo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ dependencies {
implementation "io.reactivex.rxjava2:rxkotlin:$rxkotlin_version"
implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version"

implementation project(':recycleradapter')
// Paging
def paging_version = "2.1.0"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"

implementation project(':recycleradapter')
implementation project(':recycleradapter-extensions')
}
6 changes: 5 additions & 1 deletion app/demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@
android:parentActivityName=".activities.MainActivity"/>

<activity
android:name=".activities.APIIntegration"
android:name=".activities.InfiniteScroll"
android:parentActivityName=".activities.MainActivity"/>

<activity
android:name=".activities.Carousels"
android:parentActivityName=".activities.MainActivity"/>

</application>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.gotev.recycleradapterdemo.activities

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_recycler_view.*
import net.gotev.recycleradapter.RecyclerAdapter
import net.gotev.recycleradapter.ext.NestedRecyclerAdapterItem
import net.gotev.recycleradapterdemo.R
import net.gotev.recycleradapterdemo.adapteritems.LabelItem
import net.gotev.recycleradapterdemo.adapteritems.TitledCarousel


class Carousels : AppCompatActivity() {

companion object {
private const val PARAM_POOL = "withPool"
fun show(activity: AppCompatActivity, withPool: Boolean) {
activity.startActivity(Intent(activity, Carousels::class.java).apply {
putExtra(PARAM_POOL, withPool)
})
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)

val withPool = intent.getBooleanExtra(PARAM_POOL, false)

title = getString(if (withPool) {
R.string.carousels_pool
} else {
R.string.carousels_plain
})

supportActionBar?.apply {
setHomeButtonEnabled(true)
setDisplayHomeAsUpEnabled(true)
}

val recyclerAdapter = RecyclerAdapter()

recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
recycler_view.adapter = recyclerAdapter

val recycledViewPool = if (withPool) {
RecyclerAdapter.createRecycledViewPool(
parent = recycler_view,
items = listOf(LabelItem("")),
maxViewsPerItem = 50
)
} else {
null
}

recyclerAdapter.add(createCarousels(recycledViewPool))

swipeRefresh.setOnRefreshListener {
swipeRefresh.isRefreshing = false
}

}

private fun createCarouselItems(): List<LabelItem> {
return (0..40).map {
LabelItem("Text $it")
}
}

private fun createCarousels(recycledViewPool: RecyclerView.RecycledViewPool?)
: List<NestedRecyclerAdapterItem<*>> {
return (0..60).map {
val adapter = RecyclerAdapter().apply {
add(createCarouselItems())
}

TitledCarousel("Carousel $it", adapter, recycledViewPool)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.gotev.recycleradapterdemo.activities

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_recycler_view.*
import net.gotev.recycleradapterdemo.App
import net.gotev.recycleradapterdemo.R
import net.gotev.recycleradapterdemo.network.api.StarWarsPeopleDataSource
import net.gotev.recycleradapterdemo.paging.PagingHelper


class InfiniteScroll : AppCompatActivity() {

companion object {
fun show(activity: AppCompatActivity) {
activity.startActivity(Intent(activity, InfiniteScroll::class.java))
}
}

private lateinit var pagingHelper: PagingHelper<*>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)

title = getString(R.string.infinite_scrolling)

supportActionBar?.apply {
setHomeButtonEnabled(true)
setDisplayHomeAsUpEnabled(true)
}

pagingHelper = PagingHelper(
dataSource = { StarWarsPeopleDataSource(App.starWarsClient) },
config = PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setMaxSize(50)
.build()
)

recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
pagingHelper.setupRecyclerView(recycler_view)

swipeRefresh.isRefreshing = true
pagingHelper.start(this) {
swipeRefresh.isRefreshing = false
}

swipeRefresh.setOnRefreshListener {
pagingHelper.reload()
}

}

}
Loading

0 comments on commit 8f18fd5

Please sign in to comment.