Skip to content

Latest commit

 

History

History
413 lines (307 loc) · 9.67 KB

README.md

File metadata and controls

413 lines (307 loc) · 9.67 KB

RecyclerView,Compose like

LazyRecycler is a library that provides LazyColumn like APIs to build lists with RecyclerView.

Quick start

Add the dependency Maven Central:

implementation 'io.github.dokar3:lazyrecycler:latest_version'

Then create a list by using the lazyRecycler() function. Adapter, LayoutManager, DiffUtil, OnItemClickListener and more, these are all in one block:

lazyRecycler(recyclerView, spanCount = 3) {
    // Header
    item(layout = R.layout.header) {}

    // Create a section
    items(
        items = listOfNews,
        layout = ItemNewsBinding::inflate,
        clicks = { view, item ->
            // Handle item clicks
        },
        longClicks = { view, item -> 
            // Handle long clicks
            true
        },
        diffCallback = diffCallback {
            areItemsTheSame { oldItem, newItem ->
                oldItem.id == newItem.id
            }
            areContentsTheSame { oldItem, newItem ->
                oldItem.title == newItem.title && ...
            }
        },
        span = { position ->
            if (position % 3 == 0) 3 else 1
        },
    ) { binding, news ->
        // Bind item
    }
    
    // Some other sections
    items(...)
    items(...)
    
    // Footer
    item(layout = R.layout.footer) {}
}

Basic

Sections

In LazyRecycler, every item and items call will create a section, sections will be added to the Adapter by the creating order.

item(...) { ... }

items(...) { ... }

When creating a dynamic section, it's necessary to set a unique id to update the section later, or using reactive data sources may be a better choice (see the Reactive data sources section).

val recycler = lazyRecycler {
    items(
        items = news,
        layout = R.layout.item_news,
        id = SOME_ID,
    ) { ... }
}

// Update
recycler.updateSection(SOME_ID, items)
// Remove
recycler.removeSection(SOME_ID)

Layout

// xml layout id
items(items = news, layout = R.layout.item_news) { ... }

// ViewBinding, DataBinding
items(items = news, layout = ItemNewsBinding::inflate) { binding, item -> ... }

// Instantiate views
items(items = news) { parent ->
    val itemView = NewsItemView(context)
    ...
    itemView
}

Bind

For ViewBinding item/items:

items(items = news, layout = ItemNewsBinding::inflate) { binding, item ->
    binding.title.text = item.title
    binding.image.load(item.cover)
    ...
}

For DataBinding item/items:

items(items = news, layout = ItemNewsDataBinding::inflate) { binding, item ->
    binding.news = item
}

For layoutId item/items:

items(items = news, layout = R.layout.item_news) { view ->
    ...
    val tv: TextView = view.findViewById(R.id.title)
    bind { item ->
        tv.text = item.title
    }
}

For view instantiation item/items:

items(items = news) { parent ->
    val itemView = CustomNewsItemView(context)
    bind { item ->
        itemView.title(item.title)
        ...
    }
    itemView
}

Generics

items<I>(items = news, layout = layoutId) { ... }
items<V, I>(items = news, layout = ItemViewBinding::inflate) { ... }
items<I>(items = news) { ... }
  • I for item type
  • V for the View Binding

In most cases, the compiler is smart enough so type parameter(s) can be omitted.

LayoutManager

LazyRecycler creates a LinearLayoutManager by default, if spanCount > 1, GridLayoutManager will be used, to skip the LayoutManager setup, set setupLayoutManager to false:

lazyRecycler(
    recyclerView,
    setupLayoutManager = false,
) { ... }
....
recyclerView.layoutManager = YourOwnLayoutManager(...)

span is used to define SpanSizeLookup for GridLayoutManager:

items(
    ...,
    span = { position ->
        if (position == 0) 3 else 1 
    },
) {
    ...
}

Reactive data sources

To support reactive data sources like Flow, LiveData, or RxJava, add the dependencies:

// Flow
implementation 'io.github.dokar3:lazyrecycler-flow:latest_version'
// LiveData
implementation 'io.github.dokar3:lazyrecycler-livedata:latest_version'
// RxJava3
implementation 'io.github.dokar3:lazyrecycler-rxjava3:latest_version'
// Paging 3
implementation 'io.github.dokar3:lazyrecycler-paging3:latest_version'

Latest version: Maven Central

Flow

val entry: Flow<I> = ...
item(data = entry.toMutableValue(coroutineScope), ...) { ... }

val source: Flow<List<I>> = ...
items(data = source.toMutableValue(coroutineScope), ...) { ... }

LiveData

val entry: LiveData<I> = ...
item(data = entry.toMutableValue(lifecycleOwner), ...) { ... }

val source: LiveData<List<I>> = ...
items(data = source.toMutableValue(lifecycleOwner), ...) { ... }

RxJava

val entry: Observable<I> = ...
item(data = entry.toMutableValue(), ...) { ... }

val source: Observable<List<I>> = ...
items(data = source.toMutableValue(), ...) { ... }

Paging 3

To use Paging 3 with LazyRecycler, some additional setups are required.

First, create a PagingValue from your PagingData flow.

// Required by the AsyncPagingDataDiffer
val diffCallback = object : DiffUtil.ItemCallback<Post>() {
    override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean =
        oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean =
        oldItem == newItem
}
// A PagingValue is a MutableValue so we can use it in our items() setup
val pagingValue = PagingValue(
    scope = lifecycleScope,
    // The PagingData flow: Flow<PagingData<Post>>
    flow = viewModel.postFlow,
    diffCallback = diffCallback,
)

Second, setup PagingLazyAdapter to trigger loads when the list scrolls to the end.

lazyRecycler(
    recyclerView = recyclerView,
    adapterCreator = ::PagingLazyAdapter,
) { ... }

Finally, setup items to use the paging value.

items(
    data = pagingValue,
    layout = ItemPostBinding::inflate,
    diffCallback = pagingValue.diffCallback,
) {
    // binding
}

Observe/stop observing data sources:

LazyRecycler will observe the data sources automatically after attaching to the RecyclerView, so it's no necessary to call it manually. But there is a function call if really needed:

recycler.observeChanges()

When using a RxJava data source or a Flow data source which was not created by the lifecycleScope, should stop observing data sources manually to prevent leaks:

recycler.stopObserving()

Advanced

Alternate sections

Use template() to reuse bindings:

lazyRecycler {
    // layout id template
    val sectionHeader = template<String>(R.layout.section_header) {
        // Bind item
        bind { ... }
    }
    // ViewBinding template
    val normalItem = template<Item>(ItemFriendBubbleBinding::inflate) { binding, item ->
        // Bind item
    }
    
    item(item = "section 1", template = sectionHeader)
    items(items = someItems, template = normalItem)
    
    item(item = "section 2", template = sectionHeader)
    items(items = otherItems, template = normalItem)
    ...
}

Single items with multiple view types

Use template() + extraViewTypes:

lazyRecycler {
    val friendBubble = template<Message>(ItemFriendBubbleBinding::inflate) { binding, msg ->
        // Bind alternative items
    }
    
    items(
        items = messages,
        layout = ItemOwnerBubbleBinding::inflate,
        extraViewTypes = listOf(
            ViewType(friendBubble) { position, item -> /* predicate */ },
        ),
    { binding, msg ->
        // Bind default items
    }
    ...
}

Add new sections dynamically

Use recycler.newSections():

val recycler = lazyRecycler { ... }
recycler.newSections {
    item(...) { ... }
    items(...) { ... }
    ...
}
// If new sections contain any observable data source
recycler.observeChanges()

Build list in background

backgroundThread {
    // Do not pass RecyclerView to the lazyRecycler()
    val recycler = lazyRecycler {
        ...
    }
    uiThread {
        recycler.attachTo(recyclerView)
    }
}

Demos

1. Multiple sections

Day Night

2. Chat screen

Day Night

3. Tetris game (Just for fun)

Day Night

Links

License

Apache License 2.0