diff --git a/README.md b/README.md index 111c358..a20dde9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # Recycler View Changes Detector +[V1 documentation](https://github.com/jacek-marchwicki/recyclerview-changes-detector/blob/1.0.2/README.md) + [![Build Status](https://travis-ci.org/jacek-marchwicki/recyclerview-changes-detector.svg?branch=master)](https://travis-ci.org/jacek-marchwicki/recyclerview-changes-detector) [![Jitpack Status](https://jitpack.io/v/jacek-marchwicki/recyclerview-changes-detector.svg)](https://jitpack.io/#jacek-marchwicki/recyclerview-changes-detector) -Library allow to automatically detect changes in your data and call methods: -- notifyItemRangeInserted() -- notifyItemRangeChanged() -- notifyItemRangeRemoved() -- notifyItemMoved() +Lightweight library that simplifies creation of RecyclerView's Adapter: +- Less boilerplate. No need to implement `onCreateViewHolder`, `getItemViewType`, `onBindViewHolder`, `getItemId` +- Out of the box DiffUtil support. You don't have to implement the `DiffUtil.ItemCallback()` anymore. +- Plug and play data models and view holders +- Cleaner tests +- RX support (optional) ## How it looks @@ -21,238 +24,140 @@ repositories { } dependencies { - - // UniversalAdapter with changes detector with RxJava - implementation 'com.github.jacek-marchwicki.recyclerview-changes-detector:universal-adapter-rx:' - - // UniversalAdapter with changes detector without RxJava implementation 'com.github.jacek-marchwicki.recyclerview-changes-detector:universal-adapter:' - - // Changes Detector and Adapter items (without Android dependencies) - implementation 'com.github.jacek-marchwicki.recyclerview-changes-detector:universal-adapter-java:' - - // Changes Detector (without Android dependencies) - implementation 'com.github.jacek-marchwicki.recyclerview-changes-detector:changes-detector:' + // RX support + implementation 'com.github.jacek-marchwicki.recyclerview-changes-detector:universal-adapter-rx:' } ``` ## How to use -### With Universal Adapter - -Implement some Pojo with your data: +Let's assume that your list consits of headers, songs and a footer. -```java -private class Data implements BaseAdapterItem { +- Implement data models for all list elements - private final long id; - @Nonnull - private final String name; - private final int color; +```kotlin +data class HeaderItem(val text: String, val songsCount: Int, override val itemId: Any = text) : DefaultAdapterItem() - Data(long id, @Nonnull String name, int color) { - this.id = id; - this.name = name; - this.color = color; - } - - @Override - public long adapterId() { - return id; - } - - /** - * Return true if id matches - */ - @Override - public boolean matches(@Nonnull BaseAdapterItem item) { - return item instanceof Data && (((Data) item).id == id); - } +data class SongItem(val id: String, val title: String, val imageUrl: String, override val itemId: Any = id, val onSongClick: (id: String) -> Unit) : DefaultAdapterItem() - /** - * Return true if items are equal - */ - @Override - public boolean same(@Nonnull BaseAdapterItem item) { - return equals(item); - } +data class FooterItem(override val itemId: Any = NO_ID) : DefaultAdapterItem() - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Data)) return false; - final Data data = (Data) o; - return id == data.id && - name.equals(data.name) && - color == data.color; - } -} ``` -Implement your holder: +- Implement a view holder for each data model to bind its data to the view. You have to specify item layout id and data model class. -```java -public class DataViewHolder implements ViewHolderManager { - - @Override - public boolean matches(@Nonnull BaseAdapterItem baseAdapterItem) { - return baseAdapterItem instanceof Data; - } - - @Nonnull - @Override - public BaseViewHolder createViewHolder(@Nonnull ViewGroup parent, @Nonnull LayoutInflater inflater) { - return new ViewHolder(inflater.inflate(R.layout.data_item, parent, false)); - } - - private class ViewHolder extends BaseViewHolder { - - @Nonnull - private final TextView text; - @Nonnull - private final CardView cardView; - - ViewHolder(@Nonnull View itemView) { - super(itemView); - text = (TextView) itemView.findViewById(R.id.data_item_text); - cardView = (CardView) itemView.findViewById(R.id.data_item_cardview); - } - - @Override - public void bind(@Nonnull Data item) { - text.setText(item.name); - cardView.setCardBackgroundColor(item.color); +```kotlin +class HeaderViewHolder : LayoutViewHolderManager( + R.layout.item_header, HeaderItem::class, { HeaderViewHolder(it) } +) { + class HeaderViewHolder(itemView: View) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: HeaderItem) { + itemView.item_header_tv.text = "${item.text} - ${item.songsCount}" } } } -``` - -Setup recycler view: - -```java -final UniversalAdapter adapter = new UniversalAdapter(Collections.singletonList(new DataViewHolder())); -recyclerView.setAdapter(adapter); -``` - -Give new data to adapter: - -```java -adapter.call(Arrays.toList(new Data(1, "Cow"), new Data(2, "Dg"), new Data(3, "Cat")); -``` -And another data so recycler view will be nice animated: - -```java -adapter.call(Arrays.toList(Data(2, "Dog"), new Data(3, "Cat"), new Data(4, "Elephant")); -``` - -### With auto-value (recommended) - -Usage like above with small improvement: - -```java -@AutoValue -private class Data implements BaseAdapterItem { - - @Nonnull - @AdapterId - public abstract String id(); - @Nonnull - public abstract String name(); - public abstract int color(); - - public Data create(@Nonull String id, @Nonnull String name, int color) { - return AutoValue_Data(id, name, color); +class SongViewHolder(val imageLoader: ImageLoader) : LayoutViewHolderManager( + R.layout.song_item, SongItem::class, { ViewHolder(it) } +) { + class ViewHolder(itemView: View) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: SongItem) { + itemView.song_item.text = item.title + itemView.setOnClickListener { item.onSongClick(item.id) } + imageLoader.load(item.imageUrl).into(itemView.song_cover_iv) + } } - - // methods equal, adapterId, matches and same will be generated for you } -``` - - -For more info look: [AutoValue: BaseAdapterItem Extension](https://github.com/m-zagorski/auto-value-base-adapter-item) - - -### Without Universal Adapter - -Implement some Pojo with your data: - -```java -private class Data implements SimpleDetector.Detectable { - private final long id; - @Nonnull - private final String name; - private final int color; - - Data(long id, @Nonnull String name, int color) { - this.id = id; - this.name = name; - this.color = color; - } - - /** - * Return true if id matches - */ - @Override - public boolean matches(@Nonnull Data item) { - return ((Data) item).id == id; - } - - /** - * Return true if items are equal - */ - @Override - public boolean same(@Nonnull Data item) { - return equals(item); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Data)) return false; - final Data data = (Data) o; - return id == data.id && - name.equals(data.name) && - color == data.color; +class FooterViewHolder : LayoutViewHolderManager( + R.layout.item_footer, FooterItem::class, { ViewHolder(it) } +) { + class ViewHolder(itemView: View) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: FooterItem) {} } } + ``` -Setup changes detector for your adapter +- Setup the adapter and bind data: + +```kotlin +val adapter = UniversalAdapter(listOf(headerViewHolder, songViewHolder, footerViewHolder)) +recyclerView.adapter = adapter + +// You'd rather create the items in a ViewModel/Presenter +adapter.submitList(listOf( + HeaderItem(text="Album1"), + Song(id="1", title="Song1"), + Song(id="2", title="Song2"), + HeaderItem(text="Album2"), + Song(id="3", title="Song1"), + FooterItem(), +)); +``` +## DiffUtil support +DiffUtil is an androidx tool that calculates the difference between two lists submitted to adapter. +Thanks to this, only modified elements are updated and not the whole list. It also applies a very nice animation that you can see on the GIF above. +Normally you have to implement the `DiffUtil.ItemCallback` on your own and decide when your adapter elements has changed. +Thanks to the data models you create by extending `DefaultAdapterItem` class, you don't have to do this any more. There are two conditions though +that you have to satisfy: +- you have to override `val itemId: Any` +- your data models have to be either Kotlin data classes or override `equals()` and `hashCode()` methods. + +Let's have a look at the example below: +```kotlin +data class SongItem(val id: String, val title: String, override val itemId: Any = id) : DefaultAdapterItem() ``` -@Nonnull -private final ChangesDetector changesDetector = - new ChangesDetector<>(new SimpleDetector()); +- `itemId` is required to identify specific element in the list. In this case it is the song id as it is unique to the song. This is being used in the `DiffUtil.ItemCallback.areItemsTheSame()` method. +- `SongItem` is a Kotlin data class so it overrides `equals` and `hashCode` by default. This is being used in the `DiffUtil.ItemCallback.areContentsTheSame()` method to identify whether element's content changed and need to be updated. If you'd like to alter this behaviour you can override `equals` and `hashCode` methods on your own. + +## Plug and play data models and view holders +As your models and view holders are not bound to any adapter, you can reuse them in every adapter. +You don't have to specify the `itemViewType` in each adapter, just pass you view holder to adapter's constructor. + +## Cleaner tests +As your data models are being created in a ViewModel/Presenter, your test logic is very clean and simple + +```kotlin +@Test +fun `when 2 of 4 registered students are attendees then student items correctly divided into sections`() { + every { classDao.registeredStudents } returns Observable.just( + listOf( + ClassStudent("name1", attended = true), + ClassStudent("name3", attended = false), + ClassStudent("name2", attended = true), + ClassStudent("name4", attended = false) + ) + ) + viewModel = PastClassStudentsPresenter("fake_id", classDaos) + + viewModel.adapterItems + .test() + .assertValue( + listOf( + SectionNameItem("ATTENDED"), + PastClassStudentItem("name1", true), + PastClassStudentItem("name2", true), + SectionNameItem("REGISTERED"), + PastClassStudentItem("name3", false), + PastClassStudentItem("name4", false) + ) + ) +} ``` -Give new data to your adapter: - -```java -List data = Arrays.toList(new Data(1, "Cow"), new Data(2, "Dg"), new Data(3, "Cat")); -yourAdapter.swapData(data) -changesDetector.newData(yourAdapter, data, false); +## RX support +`universal-adapter-rx` module lets you subscribe directly to adapter like this: +```kotlin +viewModel.adapterItems.subscribe(adapter) ``` -And another data so recycler view will be nice animated: - -```java -List data = Arrays.toList(Data(2, "Dog"), new Data(3, "Cat"), new Data(4, "Elephant")); -yourAdapter.swapData(data) -changesDetector.newData(yourAdapter, data, false); -``` ### More -For more: -- look on sample app at [app/](app/) directory. -- look on [AutoValue: BaseAdapterItem Extension](https://github.com/m-zagorski/auto-value-base-adapter-item) - - -### Frequently asked questions - -[FAQ](FAQ.md) +For more look at the sample app at [app/](app/) directory. ## License