├── kiel ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── java │ │ │ └── me │ │ │ │ └── ibrahimyilmaz │ │ │ │ └── kiel │ │ │ │ ├── core │ │ │ │ ├── OnBindViewHolder.kt │ │ │ │ ├── OnViewHolderCreated.kt │ │ │ │ ├── OnBindViewHolderWithPayload.kt │ │ │ │ ├── ViewHolderFactory.kt │ │ │ │ ├── RecyclerViewHolder.kt │ │ │ │ ├── RecyclerViewItemCallback.kt │ │ │ │ ├── RecyclerViewAdapterRegistry.kt │ │ │ │ ├── RecyclerViewHolderManager.kt │ │ │ │ ├── RecyclerViewHolderFactory.kt │ │ │ │ ├── RecyclerViewHolderRenderer.kt │ │ │ │ ├── AdapterRegistryBuilder.kt │ │ │ │ └── AdapterFactory.kt │ │ │ │ └── adapter │ │ │ │ ├── RecyclerViewAdapterFactory.kt │ │ │ │ └── RecyclerViewAdapter.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── me │ │ └── ibrahimyilmaz │ │ └── kiel │ │ ├── utils │ │ └── TestRecyclerViewHolder.kt │ │ ├── adapter │ │ └── RecyclerViewAdapterBuilderTest.kt │ │ └── core │ │ ├── RecyclerViewHolderManagerTest.kt │ │ ├── RecyclerViewHolderRendererTest.kt │ │ └── AdapterRegistryBuilderTest.kt ├── proguard-rules.pro └── build.gradle ├── samples ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── drawable │ │ │ ├── drawable_bg_message_item.xml │ │ │ ├── ic_circle.xml │ │ │ ├── ic_selected_message.xml │ │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── menu │ │ │ ├── menu_main.xml │ │ │ └── menu_message_list_item_selection.xml │ │ ├── layout │ │ │ ├── adapter_poll_option_item.xml │ │ │ ├── fragment_adapter_example.xml │ │ │ ├── fragment_diff_example.xml │ │ │ ├── activity_main.xml │ │ │ ├── adapter_message_text_item.xml │ │ │ ├── adapter_message_poll_item.xml │ │ │ ├── adapter_message_image_item.xml │ │ │ └── adapter_message_list_item.xml │ │ ├── navigation │ │ │ └── nav_graph.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ └── me │ │ │ └── ibrahimyilmaz │ │ │ └── kiel │ │ │ └── samples │ │ │ ├── listadapter │ │ │ ├── model │ │ │ │ ├── Message.kt │ │ │ │ ├── MessageListItemViewState.kt │ │ │ │ └── MessageSelectionState.kt │ │ │ ├── MessageActionModeCallbackListener.kt │ │ │ ├── usecase │ │ │ │ ├── FetchMessagesUseCase.kt │ │ │ │ ├── DeselectAllMessagesUseCase.kt │ │ │ │ ├── DeleteSelectedMessagesUseCase.kt │ │ │ │ ├── SelectMessageUseCase.kt │ │ │ │ ├── DeselectMessageUseCase.kt │ │ │ │ └── MessageListItemViewStateMapper.kt │ │ │ ├── viewholder │ │ │ │ └── MessageViewHolder.kt │ │ │ ├── MessageListItemSelectionActionModeCallback.kt │ │ │ ├── DiffExampleFragmentViewModel.kt │ │ │ ├── MessageRepository.kt │ │ │ └── DiffExampleFragment.kt │ │ │ ├── adapter │ │ │ ├── viewholder │ │ │ │ ├── PollOptionViewHolder.kt │ │ │ │ ├── TextMessageViewHolder.kt │ │ │ │ ├── ImageMessageViewHolder.kt │ │ │ │ └── PollMessageViewHolder.kt │ │ │ ├── model │ │ │ │ └── MessageViewState.kt │ │ │ └── AdapterExampleFragment.kt │ │ │ ├── utils │ │ │ └── MarginItemDecoration.kt │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── _config.yml ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── pull_request_template.md ├── issue_template │ ├── bug_report.md │ └── feature_request.md ├── contributing.md └── workflows │ └── build.yml ├── CHANGELOG.md ├── bintrayVersions.gradle ├── art └── kiel_icon.svg ├── gradle.properties ├── dependencies.gradle ├── .gitignore ├── gradlew.bat ├── bintray.gradle ├── README.md └── gradlew /kiel/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /kiel/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':kiel' 2 | include ':samples' 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/OnBindViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | typealias OnBindViewHolder = (VH, Int, T) -> Unit -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/OnViewHolderCreated.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | typealias OnViewHolderCreated = (VH) -> Unit 4 | -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /kiel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | / 5 | -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/kiel/develop/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/OnBindViewHolderWithPayload.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | typealias OnBindViewHolderWithPayload = (VH, Int, T, List) -> Unit -------------------------------------------------------------------------------- /samples/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | -------------------------------------------------------------------------------- /samples/src/main/res/drawable/drawable_bg_message_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/src/main/res/drawable/ic_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/model/Message.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.model 2 | 3 | data class Message( 4 | val id: Long, 5 | val sender: String, 6 | val title: String, 7 | val content: String 8 | ) 9 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/MessageActionModeCallbackListener.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter 2 | 3 | interface MessageActionModeCallbackListener { 4 | fun onDeleteActionClicked() 5 | fun onActionModeDestroyed() 6 | } 7 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/model/MessageListItemViewState.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.model 2 | 3 | data class MessageListItemViewState( 4 | val message: Message, 5 | val selectionState: MessageSelectionState 6 | ) 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 28 20:15:38 CEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /kiel/src/test/java/me/ibrahimyilmaz/kiel/utils/TestRecyclerViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.utils 2 | 3 | import android.view.View 4 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 5 | 6 | internal class TestRecyclerViewHolder( 7 | view: View 8 | ) : RecyclerViewHolder(view) 9 | -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/ViewHolderFactory.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.View 4 | 5 | typealias ViewHolderCreator = (view: View) -> VH 6 | 7 | interface ViewHolderFactory> { 8 | fun instantiate(view: View): VH 9 | } -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kiel-Samples 3 | Adapter Example 4 | DiffUtil Example 5 | Delete 6 | -------------------------------------------------------------------------------- /samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #455A64 3 | #263238 4 | #D50000 5 | 6 | #E6E5E9 7 | 8 | #CFD8DC 9 | 10 | -------------------------------------------------------------------------------- /samples/src/main/res/drawable/ic_selected_message.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes/Issues Fixed 2 | 3 | - 4 | - 5 | - 6 | ## General 7 | 8 | - [ ] Change/Fix added to `CHANGELOG.md` 9 | - [ ] Issue Link 10 | - [ ] Current Sample was changed/ new sample is introcuded 11 | 12 | ## Testing 13 | - [ ] `Unit-Test`s implemented 14 | - [ ] `Monkey-Test` done 15 | - [ ] Does your submission pass `./gradlew check`? -------------------------------------------------------------------------------- /samples/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/model/MessageSelectionState.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | sealed class MessageSelectionState : Parcelable { 7 | @Parcelize 8 | object Selected : MessageSelectionState() 9 | 10 | @Parcelize 11 | object Normal : MessageSelectionState() 12 | } 13 | -------------------------------------------------------------------------------- /.github/issue_template/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is crashing or not working as intended 4 | 5 | --- 6 | 7 | **Please complete the following information:** 8 | - Library Version [e.g. v1.0.0] 9 | - Affected Device(s) [e.g. Samsung Galaxy s10 with Android 9.0] 10 | 11 | **Describe the Bug:** 12 | 13 | Add a clear description about the problem. 14 | 15 | **Expected Behavior:** 16 | 17 | A clear description of what you expected to happen. -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/FetchMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.MessageRepository 4 | 5 | class FetchMessagesUseCase( 6 | private val repository: MessageRepository, 7 | private val mapper: MessageListItemViewStateMapper 8 | ) { 9 | 10 | suspend operator fun invoke() = repository.getMessages().map(mapper::map) 11 | } 12 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/DeselectAllMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.MessageRepository 4 | 5 | class DeselectAllMessagesUseCase( 6 | private val repository: MessageRepository, 7 | private val mapper: MessageListItemViewStateMapper 8 | ) { 9 | 10 | operator fun invoke() = repository.deselectAllMessages().map(mapper::map) 11 | } 12 | -------------------------------------------------------------------------------- /.github/issue_template/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem?** 8 | 9 | A clear and concise description of what the problem is. 10 | 11 | **Describe the solution you'd like:** 12 | 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered:** 16 | 17 | A clear description of any alternative solutions you've considered. -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/DeleteSelectedMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.MessageRepository 4 | 5 | class DeleteSelectedMessagesUseCase( 6 | private val repository: MessageRepository, 7 | private val mapper: MessageListItemViewStateMapper 8 | ) { 9 | 10 | operator fun invoke() = repository.deleteSelectedMessages().map(mapper::map) 11 | } 12 | -------------------------------------------------------------------------------- /samples/src/main/res/menu/menu_message_list_item_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to `Kiel` you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `./gradlew build` (or `gradlew.bat build` on Windows) 10 | and passing the tests by running `./gradlew check` (or `gradlew.bat check` on Windows). -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | abstract class RecyclerViewHolder(view: View) : RecyclerView.ViewHolder(view) { 7 | 8 | open fun bind( 9 | position: Int, 10 | item: @UnsafeVariance T 11 | ) = Unit 12 | 13 | open fun bind( 14 | position: Int, 15 | item: @UnsafeVariance T, 16 | payloads: List 17 | ) = bind(position, item) 18 | } -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/RecyclerViewItemCallback.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.recyclerview.widget.DiffUtil 5 | 6 | class RecyclerViewItemCallback : DiffUtil.ItemCallback() { 7 | 8 | override fun areItemsTheSame( 9 | oldItem: T, 10 | newItem: T 11 | ) = oldItem === newItem 12 | 13 | @SuppressLint("DiffUtilEquals") 14 | override fun areContentsTheSame( 15 | oldItem: T, 16 | newItem: T 17 | ) = oldItem == newItem 18 | } -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/adapter/viewholder/PollOptionViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.adapter.viewholder 2 | 3 | import android.view.View 4 | import android.widget.RadioButton 5 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 6 | import me.ibrahimyilmaz.kiel.samples.R 7 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Poll.PollOption 8 | 9 | class PollOptionViewHolder(view: View) : RecyclerViewHolder(view) { 10 | val pollOption = view.findViewById(R.id.pollOption) 11 | } 12 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/SelectMessageUseCase.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.MessageRepository 4 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.Message 5 | 6 | class SelectMessageUseCase( 7 | private val repository: MessageRepository, 8 | private val mapper: MessageListItemViewStateMapper 9 | ) { 10 | 11 | operator fun invoke( 12 | message: Message 13 | ) = repository.selectMessage(message).map(mapper::map) 14 | } 15 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/DeselectMessageUseCase.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.MessageRepository 4 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.Message 5 | 6 | class DeselectMessageUseCase( 7 | private val repository: MessageRepository, 8 | private val mapper: MessageListItemViewStateMapper 9 | ) { 10 | 11 | operator fun invoke( 12 | message: Message 13 | ) = repository.deselectMessage(message).map(mapper::map) 14 | } 15 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/adapter/viewholder/TextMessageViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.adapter.viewholder 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 6 | import me.ibrahimyilmaz.kiel.samples.R 7 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Text 8 | 9 | class TextMessageViewHolder(view: View) : RecyclerViewHolder(view) { 10 | val messageText = view.findViewById(R.id.messageText) 11 | val sentAt = view.findViewById(R.id.sentAt) 12 | } 13 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/usecase/MessageListItemViewStateMapper.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.usecase 2 | 3 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.Message 4 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageListItemViewState 5 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageSelectionState 6 | 7 | class MessageListItemViewStateMapper { 8 | fun map( 9 | entry: Map.Entry 10 | ) = MessageListItemViewState( 11 | entry.key, 12 | entry.value 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /samples/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/utils/MarginItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.utils 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration 7 | import androidx.recyclerview.widget.RecyclerView.State 8 | 9 | class MarginItemDecoration( 10 | private val spaceHeight: Int 11 | ) : ItemDecoration() { 12 | override fun getItemOffsets( 13 | outRect: Rect, 14 | view: View, 15 | parent: RecyclerView, 16 | state: State 17 | ) { 18 | with(outRect) { 19 | if (parent.getChildAdapterPosition(view) == 0) { 20 | top = spaceHeight 21 | } 22 | left = spaceHeight 23 | right = spaceHeight 24 | bottom = spaceHeight 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /kiel/src/test/java/me/ibrahimyilmaz/kiel/adapter/RecyclerViewAdapterBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.adapter 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import me.ibrahimyilmaz.kiel.utils.TestRecyclerViewHolder 5 | import org.junit.Before 6 | import org.junit.Test 7 | 8 | class RecyclerViewAdapterBuilderTest { 9 | 10 | private lateinit var recyclerViewAdapterBuilder: RecyclerViewAdapterFactory 11 | 12 | @Before 13 | fun setUp() { 14 | recyclerViewAdapterBuilder = RecyclerViewAdapterFactory() 15 | } 16 | 17 | @Test 18 | fun `Should build an instance of RecyclerViewAdapter`() { 19 | // GIVEN 20 | recyclerViewAdapterBuilder.register( 21 | viewHolder = ::TestRecyclerViewHolder, 22 | layoutResource = 1 23 | ) 24 | 25 | // WHEN 26 | val adapter = recyclerViewAdapterBuilder.create() 27 | 28 | // THEN 29 | assertThat(adapter).isInstanceOf(RecyclerViewAdapter::class.java) 30 | } 31 | } -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolderManager.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.ViewGroup 4 | import androidx.annotation.LayoutRes 5 | 6 | class RecyclerViewHolderManager>( 7 | private val recyclerViewHolderFactory: RecyclerViewHolderFactory, 8 | private val renderer: RecyclerViewHolderRenderer 9 | ) { 10 | fun instantiate( 11 | parent: ViewGroup, 12 | viewType: Int 13 | ) = recyclerViewHolderFactory.instantiate(parent, viewType) 14 | 15 | fun onBindViewHolder( 16 | holder: VH, 17 | position: Int, 18 | item: T 19 | ) = renderer.renderViewHolder(holder, position, item) 20 | 21 | fun onBindViewHolder( 22 | holder: VH, 23 | position: Int, 24 | item: T, 25 | payloads: List 26 | ) = renderer.renderViewHolder(holder, position, item, payloads) 27 | 28 | @LayoutRes 29 | fun getItemViewType(item: T): Int = renderer.getItemViewType(item) 30 | } -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolderFactory.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.collection.SimpleArrayMap 6 | 7 | class RecyclerViewHolderFactory>( 8 | private val viewHolderMap: SimpleArrayMap>, 9 | private val viewHolderCreatedListeners: SimpleArrayMap> 10 | ) { 11 | 12 | fun instantiate(parent: ViewGroup, viewType: Int): RecyclerViewHolder { 13 | val viewHolderFactory = checkNotNull(viewHolderMap[viewType]) { 14 | "ViewHolder is not found for provided viewType:$viewType" 15 | } 16 | 17 | return viewHolderFactory.instantiate( 18 | LayoutInflater.from(parent.context).inflate( 19 | viewType, 20 | parent, 21 | false 22 | ) 23 | ).also { 24 | viewHolderCreatedListeners[viewType]?.invoke(it) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/viewholder/MessageViewHolder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter.viewholder 2 | 3 | import android.view.View 4 | import androidx.core.view.isVisible 5 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 6 | import me.ibrahimyilmaz.kiel.samples.databinding.AdapterMessageListItemBinding 7 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageListItemViewState 8 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageSelectionState 9 | 10 | class MessageViewHolder(view: View) : 11 | RecyclerViewHolder(view) { 12 | val binding = AdapterMessageListItemBinding.bind(view) 13 | var boundItem: MessageListItemViewState? = null 14 | 15 | override fun bind(position: Int, item: MessageListItemViewState) { 16 | boundItem = item 17 | } 18 | 19 | override fun bind(position: Int, item: MessageListItemViewState, payloads: List) { 20 | boundItem = item 21 | } 22 | 23 | fun setSelectionState(messageSelectionState: MessageSelectionState) { 24 | itemView.isSelected = messageSelectionState == MessageSelectionState.Selected 25 | binding.selectedImage.isVisible = itemView.isSelected 26 | binding.imageLetter.isVisible = !itemView.isSelected 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/MessageListItemSelectionActionModeCallback.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter 2 | 3 | import android.view.ActionMode 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import me.ibrahimyilmaz.kiel.samples.R 7 | 8 | class MessageListItemSelectionActionModeCallback( 9 | private val listener: MessageActionModeCallbackListener 10 | ) : ActionMode.Callback { 11 | override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = 12 | when (item?.itemId) { 13 | R.id.deleteItem -> { 14 | listener.onDeleteActionClicked() 15 | mode?.finish() 16 | true 17 | } 18 | else -> false 19 | } 20 | 21 | override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { 22 | val actionMode = mode ?: return false 23 | val actionMenu = menu ?: return false 24 | actionMode.menuInflater.inflate(R.menu.menu_message_list_item_selection, actionMenu) 25 | return true 26 | } 27 | 28 | override fun onPrepareActionMode( 29 | mode: ActionMode?, 30 | menu: Menu? 31 | ) = false 32 | 33 | override fun onDestroyActionMode( 34 | mode: ActionMode? 35 | ) = listener.onActionModeDestroyed() 36 | } 37 | -------------------------------------------------------------------------------- /samples/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 19 | 26 | 27 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext.versions = [ 2 | minSdk : 19, 3 | compileSdk : 29, 4 | versionCode : 0, 5 | versionName : '0.99.99', 6 | 7 | // gradle plugins 8 | gradleBuildTool : '4.0.0', 9 | versionPlugin : '0.28.0', 10 | ktlintPluginVersion : '9.3.0', 11 | 12 | // kotlin 13 | kotlin : '1.3.72', 14 | 15 | // android 16 | multidexVersion : '2.0.1', 17 | recyclerViewVersion : '1.1.0', 18 | pagingAdapterVersion : '3.0.0-alpha03', 19 | androidxCore : '1.3.1', 20 | appCompatVersion : '1.1.0', 21 | constraintVersion : '1.1.3', 22 | navigationVersion : "2.3.0", 23 | materialVersion : "1.1.0", 24 | 25 | // glide 26 | glideVersion : '4.11.0', 27 | glidePaletteVersion : '2.1.2', 28 | 29 | // unit test 30 | truthVersion : '1.0.1', 31 | junitVersion : '4.13', 32 | mockitoKotlinVersion : '2.2.0', 33 | mockitoInlineVersion : '3.3.3', 34 | androidxTestJunit : '1.1.1', 35 | espressoVersion : '3.2.0', 36 | 37 | // deployment 38 | mavenGradlePluginVersion : '2.1', 39 | gradleBintrayPluginVersion: '1.8.4', 40 | ] -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolderRenderer.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import androidx.collection.SimpleArrayMap 4 | 5 | class RecyclerViewHolderRenderer>( 6 | private val itemTypes: 7 | SimpleArrayMap, Int>, 8 | private val bindViewHolderListeners: 9 | SimpleArrayMap, OnBindViewHolder>>, 10 | private val viewHolderBoundWithPayloadListeners: 11 | SimpleArrayMap, OnBindViewHolderWithPayload>> 12 | ) { 13 | 14 | fun getItemViewType(t: T) = checkNotNull(itemTypes[t.javaClass]) { 15 | "Item Type is not defined for ${t.javaClass.name}" 16 | } 17 | 18 | fun renderViewHolder( 19 | holder: VH, 20 | position: Int, 21 | item: T 22 | ) = holder.bind(position, item).also { 23 | bindViewHolderListeners[item.javaClass]?.invoke(holder, position, item) 24 | } 25 | 26 | fun renderViewHolder( 27 | holder: VH, 28 | position: Int, 29 | item: T, 30 | payloads: List 31 | ) { 32 | if (payloads.isEmpty()) { 33 | renderViewHolder(holder, position, item) 34 | } else { 35 | holder.bind(position, item, payloads).also { 36 | viewHolderBoundWithPayloadListeners[item.javaClass]?.invoke( 37 | holder, 38 | position, 39 | item, 40 | payloads 41 | ) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /samples/src/main/res/layout/adapter_message_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 25 | 26 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | -------------------------------------------------------------------------------- /samples/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /samples/src/main/res/layout/adapter_message_poll_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 23 | 24 | 31 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.annotation.IdRes 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.drawerlayout.widget.DrawerLayout 8 | import androidx.navigation.NavController 9 | import androidx.navigation.fragment.NavHostFragment 10 | import androidx.navigation.ui.AppBarConfiguration 11 | import androidx.navigation.ui.navigateUp 12 | import androidx.navigation.ui.onNavDestinationSelected 13 | import androidx.navigation.ui.setupActionBarWithNavController 14 | import androidx.navigation.ui.setupWithNavController 15 | import com.google.android.material.navigation.NavigationView 16 | 17 | class MainActivity : AppCompatActivity() { 18 | private lateinit var navController: NavController 19 | private lateinit var appBarConfiguration: AppBarConfiguration 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | 24 | navController = findNavController(R.id.nav_host_fragment) 25 | 26 | findViewById(R.id.nav_view) 27 | .setupWithNavController(navController) 28 | 29 | appBarConfiguration = AppBarConfiguration( 30 | navController.graph, 31 | findViewById(R.id.drawer_layout) 32 | ) 33 | setupActionBarWithNavController(navController, appBarConfiguration) 34 | } 35 | 36 | private fun findNavController( 37 | @Suppress("SameParameterValue") @IdRes id: Int 38 | ) = (supportFragmentManager.findFragmentById(id) as NavHostFragment).navController 39 | 40 | override fun onOptionsItemSelected(item: MenuItem) = 41 | item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item) 42 | 43 | override fun onSupportNavigateUp() = 44 | navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 45 | } 46 | -------------------------------------------------------------------------------- /samples/src/main/res/layout/adapter_message_image_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 26 | 27 | 37 | 38 | 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" 7 | 8 | jobs: 9 | jvm: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | java-version: 16 | - 1.8 17 | - 9 18 | - 10 19 | - 11 20 | - 12 21 | - 13 22 | - 14 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Validate Gradle Wrapper 29 | uses: gradle/wrapper-validation-action@v1 30 | 31 | - name: Configure JDK 32 | uses: actions/setup-java@v1 33 | with: 34 | java-version: ${{ matrix.java-version }} 35 | 36 | - name: Check 37 | run: ./gradlew check 38 | 39 | - name: ktlint Check 40 | run: ./gradlew --continue ktlintCheck 41 | 42 | - name: Test 43 | run: ./gradlew build 44 | 45 | android: 46 | runs-on: macos-latest 47 | 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | api-level: 52 | - 21 53 | - 24 54 | - 26 55 | - 29 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v2 60 | 61 | - name: Validate Gradle Wrapper 62 | uses: gradle/wrapper-validation-action@v1 63 | 64 | - name: Run Tests 65 | uses: reactivecircus/android-emulator-runner@v2 66 | with: 67 | api-level: ${{ matrix.api-level }} 68 | script: ./gradlew connectedCheck 69 | env: 70 | API_LEVEL: ${{ matrix.api-level }} 71 | 72 | publish: 73 | runs-on: ubuntu-latest 74 | if: github.ref == 'refs/heads/master' 75 | needs: 76 | - jvm 77 | - android 78 | 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v2 82 | 83 | - name: Configure JDK 84 | uses: actions/setup-java@v1 85 | with: 86 | java-version: 14 87 | 88 | - name: Upload Artifacts 89 | run: ./gradlew clean install bintrayUpload 90 | env: 91 | BINTRAY_APIKEY: ${{ secrets.BINTRAY_APIKEY }} 92 | BINTRAY_USER: ${{ secrets.BINTRAY_USER }} -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/adapter/RecyclerViewAdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.adapter 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import me.ibrahimyilmaz.kiel.core.AdapterFactory 5 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 6 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolderFactory 7 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolderManager 8 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolderRenderer 9 | 10 | typealias AreItemsTheSame = (old: T, new: T) -> Boolean 11 | typealias AreContentsTheSame = (old: T, new: T) -> Boolean 12 | typealias GetChangePayload = (oldItem: T, newItem: T) -> Any? 13 | 14 | class RecyclerViewAdapterFactory : 15 | AdapterFactory>>() { 16 | 17 | private var itemDiffUtil: DiffUtil.ItemCallback? = null 18 | 19 | fun diff(lambda: () -> DiffUtil.ItemCallback) { 20 | this.itemDiffUtil = lambda() 21 | } 22 | 23 | fun diff( 24 | areItemsTheSame: AreItemsTheSame, 25 | areContentsTheSame: AreContentsTheSame, 26 | getChangePayload: GetChangePayload? = null 27 | ) { 28 | itemDiffUtil = object : DiffUtil.ItemCallback() { 29 | override fun areItemsTheSame( 30 | oldItem: T, 31 | newItem: T 32 | ) = areItemsTheSame(oldItem, newItem) 33 | 34 | override fun areContentsTheSame( 35 | oldItem: T, 36 | newItem: T 37 | ) = areContentsTheSame(oldItem, newItem) 38 | 39 | override fun getChangePayload(oldItem: T, newItem: T) = 40 | getChangePayload?.let { it(oldItem, newItem) } ?: super.getChangePayload( 41 | oldItem, 42 | newItem 43 | ) 44 | } 45 | } 46 | 47 | override fun create() = 48 | RecyclerViewAdapter>( 49 | RecyclerViewHolderManager( 50 | RecyclerViewHolderFactory( 51 | viewHolderMap, 52 | viewHolderCreatedListeners 53 | ), 54 | RecyclerViewHolderRenderer( 55 | layoutResourceMap, 56 | onBindViewHolderListeners, 57 | onBindViewHolderWithPayloadListeners 58 | ) 59 | ), 60 | itemDiffUtil 61 | ) 62 | } -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/adapter/RecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 5 | import androidx.recyclerview.widget.ListAdapter 6 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder 7 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolderManager 8 | import me.ibrahimyilmaz.kiel.core.RecyclerViewItemCallback 9 | 10 | class RecyclerViewAdapter>( 11 | private val recyclerViewHolderManager: RecyclerViewHolderManager, 12 | diffUtilItemCallback: ItemCallback = RecyclerViewItemCallback() 13 | ) : ListAdapter(diffUtilItemCallback) { 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | override fun onCreateViewHolder( 17 | parent: ViewGroup, 18 | viewType: Int 19 | ): VH = recyclerViewHolderManager.instantiate( 20 | parent, 21 | viewType 22 | ) as VH 23 | 24 | override fun onBindViewHolder( 25 | holder: VH, 26 | position: Int 27 | ) = recyclerViewHolderManager.onBindViewHolder( 28 | holder, 29 | position, 30 | getItem(position) 31 | ) 32 | 33 | override fun onBindViewHolder( 34 | holder: VH, 35 | position: Int, 36 | payloads: MutableList 37 | ) = recyclerViewHolderManager.onBindViewHolder( 38 | holder, 39 | position, 40 | getItem(position), 41 | payloads 42 | ) 43 | 44 | override fun getItemViewType( 45 | position: Int 46 | ) = recyclerViewHolderManager.getItemViewType(getItem(position)) 47 | 48 | companion object { 49 | 50 | @JvmStatic 51 | operator fun > invoke( 52 | recyclerViewHolderManager: RecyclerViewHolderManager, 53 | diffUtilCallbackFactory: ItemCallback? 54 | ) = diffUtilCallbackFactory?.let { 55 | RecyclerViewAdapter( 56 | recyclerViewHolderManager, 57 | it 58 | ) 59 | } ?: RecyclerViewAdapter( 60 | recyclerViewHolderManager 61 | ) 62 | 63 | @JvmStatic 64 | inline fun adapterOf( 65 | function: RecyclerViewAdapterFactory.() -> Unit 66 | ): RecyclerViewAdapter> = 67 | RecyclerViewAdapterFactory() 68 | .apply(function).create() 69 | } 70 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kiel/src/test/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolderManagerTest.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.ViewGroup 4 | import com.nhaarman.mockitokotlin2.mock 5 | import com.nhaarman.mockitokotlin2.verify 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import org.mockito.InjectMocks 9 | import org.mockito.Mock 10 | import org.mockito.junit.MockitoJUnitRunner 11 | 12 | @RunWith(MockitoJUnitRunner::class) 13 | class RecyclerViewHolderManagerTest { 14 | 15 | @Mock 16 | private lateinit var viewHolderFactory: RecyclerViewHolderFactory> 17 | 18 | @Mock 19 | private lateinit var renderer: RecyclerViewHolderRenderer> 20 | 21 | @InjectMocks 22 | private lateinit var viewHolderManager: RecyclerViewHolderManager> 23 | 24 | @Test 25 | fun `Should instantiate ViewHolder with provided viewType and parent`() { 26 | // GIVEN 27 | val parent = mock() 28 | val viewType = 1 29 | 30 | // WHEN 31 | viewHolderManager.instantiate(parent, viewType) 32 | 33 | // THEN 34 | verify(viewHolderFactory).instantiate(parent, viewType) 35 | } 36 | 37 | @Test 38 | fun `Should bind ViewHolder with the given viewHolder and item`() { 39 | // GIVEN 40 | val viewHolder = mock>() 41 | val position = 1 42 | val item = mock() 43 | 44 | // WHEN 45 | viewHolderManager.onBindViewHolder(viewHolder, position, item) 46 | 47 | // THEN 48 | verify(renderer).renderViewHolder(viewHolder, position, item) 49 | } 50 | 51 | @Test 52 | fun `Should bind ViewHolder with the given viewHolder, item and payloads`() { 53 | // GIVEN 54 | val viewHolder = mock>() 55 | val position = 1 56 | val item = mock() 57 | val payloads = mock>() 58 | 59 | // WHEN 60 | viewHolderManager.onBindViewHolder(viewHolder, position, item, payloads) 61 | 62 | // THEN 63 | verify(renderer).renderViewHolder(viewHolder, position, item, payloads) 64 | } 65 | 66 | @Test 67 | fun `Should return viewType for the given item`() { 68 | // GIVEN 69 | val item = mock() 70 | 71 | // WHEN 72 | viewHolderManager.getItemViewType(item) 73 | 74 | // THEN 75 | verify(renderer).getItemViewType(item) 76 | } 77 | } -------------------------------------------------------------------------------- /kiel/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "org.jlleitschuh.gradle.ktlint-idea" 2 | apply plugin: 'com.android.library' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-android-extensions' 5 | apply from: '../bintray.gradle' 6 | 7 | android { 8 | compileSdkVersion versions.compileSdk 9 | 10 | defaultConfig { 11 | minSdkVersion versions.minSdk 12 | targetSdkVersion versions.compileSdk 13 | versionCode versions.versionCode 14 | versionName versions.versionName 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | compileOptions { 19 | sourceCompatibility JavaVersion.VERSION_1_8 20 | targetCompatibility JavaVersion.VERSION_1_8 21 | } 22 | 23 | kotlinOptions { 24 | jvmTarget = JavaVersion.VERSION_1_8.toString() 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | 34 | testOptions.unitTests { 35 | returnDefaultValues = true 36 | 37 | includeAndroidResources = true 38 | 39 | all { 40 | testLogging { 41 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 42 | } 43 | } 44 | } 45 | 46 | packagingOptions { 47 | exclude 'META-INF/DEPENDENCIES' 48 | exclude 'META-INF/LICENSE' 49 | exclude 'META-INF/LICENSE.txt' 50 | exclude 'META-INF/license.txt' 51 | exclude 'META-INF/NOTICE' 52 | exclude 'META-INF/NOTICE.txt' 53 | exclude 'META-INF/notice.txt' 54 | exclude 'META-INF/ASL2.0' 55 | exclude("META-INF/*.kotlin_module") 56 | } 57 | } 58 | 59 | dependencies { 60 | implementation fileTree(dir: "libs", include: ["*.jar"]) 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" 62 | implementation "androidx.recyclerview:recyclerview:$versions.recyclerViewVersion" 63 | implementation "androidx.paging:paging-runtime:$versions.pagingAdapterVersion" 64 | 65 | testImplementation "junit:junit:$versions.junitVersion" 66 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockitoKotlinVersion" 67 | testImplementation "org.mockito:mockito-inline:$versions.mockitoInlineVersion" 68 | testImplementation "com.google.truth:truth:$versions.truthVersion" 69 | } 70 | 71 | 72 | ktlint { 73 | version = "0.37.1" 74 | debug = true 75 | verbose = true 76 | android = true 77 | outputToConsole = true 78 | outputColorName = "RED" 79 | ignoreFailures = false 80 | enableExperimentalRules = true 81 | disabledRules = ["final-newline"] 82 | reporters { 83 | reporter "html" 84 | reporter "plain" 85 | reporter "checkstyle" 86 | } 87 | filter { 88 | exclude("**/generated/**") 89 | include("**/kotlin/**") 90 | } 91 | } -------------------------------------------------------------------------------- /samples/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion versions.compileSdk 8 | defaultConfig { 9 | applicationId "me.ibrahimyilmaz.kiel.samples" 10 | minSdkVersion versions.minSdk 11 | targetSdkVersion versions.compileSdk 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | compileOptions { 18 | sourceCompatibility JavaVersion.VERSION_1_8 19 | targetCompatibility JavaVersion.VERSION_1_8 20 | } 21 | 22 | kotlinOptions { 23 | jvmTarget = JavaVersion.VERSION_1_8.toString() 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | 33 | buildFeatures { 34 | viewBinding true 35 | } 36 | } 37 | 38 | repositories { 39 | maven { url "https://dl.bintray.com/vitorhugods/AvatarView" } 40 | } 41 | 42 | 43 | dependencies { 44 | implementation fileTree(dir: "libs", include: ["*.jar"]) 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" 46 | implementation "com.google.android.material:material:$versions.materialVersion" 47 | implementation "androidx.core:core-ktx:$versions.androidxCore" 48 | implementation "androidx.appcompat:appcompat:$versions.appCompatVersion" 49 | implementation "androidx.constraintlayout:constraintlayout:$versions.constraintVersion" 50 | implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigationVersion" 51 | implementation "androidx.navigation:navigation-ui-ktx:$versions.navigationVersion" 52 | implementation "androidx.recyclerview:recyclerview:$versions.recyclerViewVersion" 53 | def lifecycle_version = "2.2.0-rc03" 54 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 55 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 56 | kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" 57 | 58 | implementation project(path: ':kiel') 59 | // Glide 60 | implementation "com.github.bumptech.glide:glide:$versions.glideVersion" 61 | implementation "com.github.florent37:glidepalette:$versions.glidePaletteVersion" 62 | kapt "com.github.bumptech.glide:compiler:$versions.glideVersion" 63 | 64 | //CircleImageView 65 | implementation 'de.hdodenhof:circleimageview:3.1.0' 66 | implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' 67 | 68 | 69 | testImplementation "junit:junit:$versions.junitVersion" 70 | androidTestImplementation "androidx.test.ext:junit:$versions.androidxTestJunit" 71 | androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espressoVersion" 72 | } -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/DiffExampleFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.map 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.launch 9 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageListItemViewState 10 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageSelectionState 11 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.DeleteSelectedMessagesUseCase 12 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.DeselectAllMessagesUseCase 13 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.DeselectMessageUseCase 14 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.FetchMessagesUseCase 15 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.MessageListItemViewStateMapper 16 | import me.ibrahimyilmaz.kiel.samples.listadapter.usecase.SelectMessageUseCase 17 | 18 | class DiffExampleFragmentViewModel : ViewModel() { 19 | 20 | private val repository = MessageRepository() 21 | private val viewStateMapper = MessageListItemViewStateMapper() 22 | private val fetchMessagesUseCase = FetchMessagesUseCase(repository, viewStateMapper) 23 | private val selectMessageUseCase = SelectMessageUseCase(repository, viewStateMapper) 24 | private val deselectMessageUseCase = DeselectMessageUseCase(repository, viewStateMapper) 25 | private val deselectAllMessagesUseCase = DeselectAllMessagesUseCase(repository, viewStateMapper) 26 | 27 | private val deleteSelectedMessagesUseCase = 28 | DeleteSelectedMessagesUseCase(repository, viewStateMapper) 29 | 30 | private val _messages = MutableLiveData>() 31 | 32 | val messages: LiveData> get() = _messages 33 | 34 | val showSelectedMessageCount: LiveData 35 | get() = _messages.map { 36 | it.count { message -> 37 | message.selectionState == MessageSelectionState.Selected 38 | } 39 | } 40 | 41 | val showDeleteAction: LiveData 42 | get() = _messages.map { 43 | it.any { message -> 44 | message.selectionState == MessageSelectionState.Selected 45 | } 46 | } 47 | 48 | init { 49 | getMessages() 50 | } 51 | 52 | private fun getMessages() { 53 | viewModelScope.launch { 54 | _messages.value = fetchMessagesUseCase() 55 | } 56 | } 57 | 58 | fun onItemLongClicked(item: MessageListItemViewState) { 59 | _messages.value = when (item.selectionState) { 60 | MessageSelectionState.Selected -> deselectMessageUseCase(item.message) 61 | MessageSelectionState.Normal -> selectMessageUseCase(item.message) 62 | } 63 | } 64 | 65 | fun onDeleteActionClicked() { 66 | _messages.value = deleteSelectedMessagesUseCase() 67 | } 68 | 69 | fun onActionDeleteModeFinished() { 70 | _messages.value = deselectAllMessagesUseCase() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/src/main/res/layout/adapter_message_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 24 | 25 | 30 | 31 | 37 | 38 | 45 | 46 | 47 | 48 | 57 | 58 | 68 | 69 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/MessageRepository.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter 2 | 3 | import kotlinx.coroutines.Dispatchers.IO 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.withContext 6 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.Message 7 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageSelectionState 8 | 9 | class MessageRepository { 10 | 11 | private var messages = mutableMapOf() 12 | 13 | suspend fun getMessages(): Map = withContext(IO) { 14 | if (messages.isEmpty()) { 15 | delay(500) 16 | (0..SAMPLE_MESSAGE_COUNT).forEach { 17 | val message = Message( 18 | it.toLong(), 19 | SENDERS[it % SENDERS.size], 20 | TITLE[it % TITLE.size], 21 | CONTENT[it % CONTENT.size] 22 | ) 23 | messages[message] = MessageSelectionState.Normal 24 | } 25 | } 26 | 27 | messages 28 | } 29 | 30 | fun selectMessage(message: Message): Map { 31 | messages[message] = MessageSelectionState.Selected 32 | return messages 33 | } 34 | 35 | fun deselectMessage(message: Message): Map { 36 | messages[message] = MessageSelectionState.Normal 37 | return messages 38 | } 39 | 40 | fun deleteSelectedMessages(): Map { 41 | val selectedMessages = 42 | messages.asSequence().filter { it.value == MessageSelectionState.Selected } 43 | .map { it.key }.toList() 44 | selectedMessages.forEach { 45 | messages.remove(it) 46 | } 47 | return messages 48 | } 49 | 50 | fun deselectAllMessages(): Map { 51 | messages.keys.forEach { 52 | messages[it] = MessageSelectionState.Normal 53 | } 54 | return messages 55 | } 56 | 57 | private companion object { 58 | private const val SAMPLE_MESSAGE_COUNT = 10 59 | private val SENDERS = 60 | arrayOf("Christian Dalonzo", "Amy Worrall", "Brendan Aronoff", "Hailey Mustermann") 61 | private val TITLE = arrayOf( 62 | "Re: Something1", 63 | "Birthday Party", 64 | "Say YES to Liverpool Game", 65 | "Gigi has invited you to work" 66 | ) 67 | private val CONTENT = arrayOf( 68 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", 69 | "Hodor. Hodor hodor, hodor. Hodor hodor hodor hodor hodor. Hodor. Hodor! Hodor hodor, hodor; hodor hodor hodor. Hodor. Hodor", 70 | "Lorem Ipsum is the single greatest threat. We are not - we are not keeping up with other websites.", 71 | "Lorem ipsum dolor amet mustache knausgaard +1, blue bottle waistcoat tbh semiotics artisan synth stumptown gastropub cornhole celiac swag", 72 | "Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro." 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/AdapterRegistryBuilder.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.View 4 | import androidx.annotation.LayoutRes 5 | 6 | @Deprecated( 7 | "In general we try to avoid builder classes in Kotlin," + 8 | " as they're not needed and introduce nullability." + 9 | " So AdapterRegistryBuilder will be retired soon" 10 | ) 11 | class AdapterRegistryBuilder { 12 | private var type: Class<*>? = null 13 | private var viewHolderIntrospection: ViewHolderFactory>? = null 14 | 15 | @LayoutRes 16 | private var layoutResource: Int? = null 17 | 18 | private var onViewHolderCreated: OnViewHolderCreated>? = null 19 | private var onViewHolderBound: OnBindViewHolder>? = null 20 | private var onViewHolderBoundWithPayload: 21 | OnBindViewHolderWithPayload>? = null 22 | 23 | fun type(lambda: () -> Class<*>) { 24 | this.type = lambda() 25 | } 26 | 27 | fun viewHolder(lambda: ViewHolderCreator>) { 28 | this.viewHolderIntrospection = object : ViewHolderFactory> { 29 | override fun instantiate(view: View): RecyclerViewHolder = lambda(view) 30 | } 31 | } 32 | 33 | fun > onViewHolderCreated(lambda: (VH) -> Unit) { 34 | onViewHolderCreated = object : 35 | OnViewHolderCreated> { 36 | @Suppress("UNCHECKED_CAST") 37 | override fun invoke(viewHolder: RecyclerViewHolder) { 38 | lambda(viewHolder as VH) 39 | } 40 | } 41 | } 42 | 43 | fun > onViewHolderBound(lambda: (VH, Int, IT) -> Unit) { 44 | onViewHolderBound = object : 45 | OnBindViewHolder> { 46 | @Suppress("UNCHECKED_CAST") 47 | override fun invoke(viewHolder: RecyclerViewHolder, position: Int, item: T) { 48 | lambda(viewHolder as VH, position, item as IT) 49 | } 50 | } 51 | } 52 | 53 | fun > onViewHolderBoundWithPayload( 54 | lambda: (VH, Int, IT, List) -> Unit 55 | ) { 56 | onViewHolderBoundWithPayload = 57 | object : 58 | OnBindViewHolderWithPayload> { 59 | @Suppress("UNCHECKED_CAST") 60 | override fun invoke( 61 | viewHolder: RecyclerViewHolder, 62 | position: Int, 63 | item: T, 64 | payloads: List 65 | ) { 66 | lambda(viewHolder as VH, position, item as IT, payloads) 67 | } 68 | } 69 | } 70 | 71 | fun layoutResource(lambda: () -> Int) { 72 | this.layoutResource = lambda() 73 | } 74 | 75 | fun build() = RecyclerViewAdapterRegistry( 76 | requireNotNull(type) { 77 | "type should be provided!" 78 | }, 79 | requireNotNull(viewHolderIntrospection) { 80 | "viewHolder constructor method should be provided!" 81 | }, 82 | requireNotNull(layoutResource) { 83 | "layout resource should be provided!" 84 | }, 85 | onViewHolderCreated, 86 | onViewHolderBound, 87 | onViewHolderBoundWithPayload 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | apply from: '../bintrayVersions.gradle' 5 | 6 | group = bintray.publishedGroupId // Maven Group ID for the artifact 7 | version = versions.versionName 8 | 9 | if (project.hasProperty("android")) { // Android libraries 10 | task sourcesJar(type: Jar) { 11 | classifier = 'sources' 12 | from android.sourceSets.main.java.srcDirs 13 | } 14 | 15 | task javadoc(type: Javadoc) { 16 | source = android.sourceSets.main.java.srcDirs 17 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 18 | failOnError false 19 | } 20 | } else { // Java libraries 21 | task sourcesJar(type: Jar, dependsOn: classes) { 22 | classifier = 'sources' 23 | from sourceSets.main.allSource 24 | } 25 | } 26 | 27 | task javadocJar(type: Jar, dependsOn: javadoc) { 28 | classifier = 'javadoc' 29 | from javadoc.destinationDir 30 | } 31 | 32 | artifacts { 33 | archives javadocJar 34 | archives sourcesJar 35 | } 36 | 37 | install { 38 | repositories.mavenInstaller { 39 | // This generates POM.xml with proper parameters 40 | pom { 41 | project { 42 | packaging 'aar' 43 | groupId bintray.publishedGroupId 44 | artifactId bintray.artifact 45 | 46 | // Add your description here 47 | description bintray.libraryDescription 48 | } 49 | } 50 | } 51 | } 52 | 53 | publishing { 54 | publications { 55 | MyPublication(MavenPublication) { 56 | pom.setPackaging('aar') 57 | pom.withXml { 58 | def dependenciesNode = asNode().appendNode('dependencies') 59 | // Iterate over the implementation dependencies (we don't want the test ones), adding a node for each 60 | configurations.implementation.allDependencies.each { 61 | // Ensure dependencies such as fileTree are not included. 62 | if (it.name != 'unspecified') { 63 | def dependencyNode = dependenciesNode.appendNode('dependency') 64 | dependencyNode.appendNode('groupId', it.group) 65 | dependencyNode.appendNode('artifactId', it.name) 66 | dependencyNode.appendNode('version', it.version) 67 | } 68 | } 69 | 70 | asNode().appendNode('url', bintray.siteUrl) 71 | 72 | def licensesNode = asNode().appendNode('licenses') 73 | def licenseNode = licensesNode.appendNode('license') 74 | licenseNode.appendNode('name', bintray.licenseName) 75 | licenseNode.appendNode('url', bintray.licenseUrl) 76 | 77 | def scmNode = asNode().appendNode('scm') 78 | scmNode.appendNode('connection', bintray.gitUrl) 79 | scmNode.appendNode('developerConnection', bintray.gitUrl) 80 | scmNode.appendNode('url', bintray.siteUrl) 81 | } 82 | } 83 | } 84 | } 85 | 86 | bintray { 87 | // Bintray 88 | Properties properties = new Properties() 89 | File localPropertiesFile = project.rootProject.file('local.properties') 90 | if (localPropertiesFile.exists()) { 91 | properties.load(localPropertiesFile.newDataInputStream()) 92 | } 93 | 94 | user = System.getenv('BINTRAY_USER') 95 | key = System.getenv('BINTRAY_APIKEY') 96 | 97 | configurations = ['archives'] 98 | publications = ['MyPublication'] 99 | pkg { 100 | repo = bintray.bintrayRepo 101 | name = bintray.bintrayName 102 | desc = bintray.libraryDescription 103 | websiteUrl = bintray.siteUrl 104 | vcsUrl = bintray.gitUrl 105 | licenses = bintray.allLicenses 106 | publish = true 107 | publicDownloadNumbers = true 108 | version { 109 | desc = bintray.libraryDescription 110 | gpg { 111 | sign = true // Determines whether to GPG sign the files. The default is false 112 | passphrase = properties.getProperty("bintray.gpg.password") 113 | // Optional. The passphrase for GPG signing 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/adapter/AdapterExampleFragment.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.adapter 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.bumptech.glide.Glide 8 | import me.ibrahimyilmaz.kiel.adapter.RecyclerViewAdapter.Companion.adapterOf 9 | import me.ibrahimyilmaz.kiel.samples.R 10 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState 11 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Image 12 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Poll 13 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Poll.PollOption 14 | import me.ibrahimyilmaz.kiel.samples.adapter.model.MessageViewState.Text 15 | import me.ibrahimyilmaz.kiel.samples.adapter.viewholder.ImageMessageViewHolder 16 | import me.ibrahimyilmaz.kiel.samples.adapter.viewholder.PollMessageViewHolder 17 | import me.ibrahimyilmaz.kiel.samples.adapter.viewholder.PollOptionViewHolder 18 | import me.ibrahimyilmaz.kiel.samples.adapter.viewholder.TextMessageViewHolder 19 | import me.ibrahimyilmaz.kiel.samples.utils.MarginItemDecoration 20 | 21 | class AdapterExampleFragment : Fragment(R.layout.fragment_adapter_example) { 22 | 23 | private lateinit var messagesRecyclerView: RecyclerView 24 | 25 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 26 | super.onViewCreated(view, savedInstanceState) 27 | messagesRecyclerView = view.findViewById(R.id.messagesRecyclerView) 28 | 29 | val recyclerViewAdapter = 30 | adapterOf { 31 | register( 32 | layoutResource = R.layout.adapter_message_text_item, 33 | viewHolder = ::TextMessageViewHolder, 34 | onBindBindViewHolder = { vh, _, text -> 35 | vh.messageText.text = text.text 36 | vh.sentAt.text = text.sentAt 37 | } 38 | ) 39 | 40 | register( 41 | layoutResource = R.layout.adapter_message_image_item, 42 | viewHolder = ::ImageMessageViewHolder, 43 | onBindBindViewHolder = { vh, _, item -> 44 | vh.messageText.text = item.text 45 | vh.sentAt.text = item.sentAt 46 | 47 | Glide.with(vh.messageImage) 48 | .load(item.imageUrl) 49 | .into(vh.messageImage) 50 | } 51 | ) 52 | 53 | register( 54 | layoutResource = R.layout.adapter_message_poll_item, 55 | viewHolder = ::PollMessageViewHolder, 56 | onBindBindViewHolder = { vh, _, poll -> 57 | vh.pollTitle.text = poll.text 58 | vh.sentAt.text = poll.sentAt 59 | 60 | val pollOptionsAdapter = adapterOf { 61 | register( 62 | layoutResource = R.layout.adapter_poll_option_item, 63 | viewHolder = ::PollOptionViewHolder, 64 | onBindBindViewHolder = { pollOptionViewHolder, _, pollOption -> 65 | pollOptionViewHolder.pollOption.text = pollOption.text 66 | } 67 | ) 68 | } 69 | 70 | vh.pollOptionsRecyclerView.adapter = pollOptionsAdapter 71 | 72 | pollOptionsAdapter.submitList(poll.options) 73 | } 74 | ) 75 | } 76 | 77 | messagesRecyclerView.adapter = 78 | recyclerViewAdapter 79 | 80 | messagesRecyclerView.addItemDecoration( 81 | MarginItemDecoration( 82 | resources.getDimension(R.dimen.padding_normal).toInt() 83 | ) 84 | ) 85 | val data = listOf( 86 | Text( 87 | "Hi David! I have landed to Earth! Human-kind is really interesting", 88 | "09:10 AM" 89 | ), 90 | Image( 91 | "Looks amazing, isn't it?", 92 | "http://lorempixel.com/400/400/nature/", 93 | "09:10 AM" 94 | ), 95 | Poll( 96 | "Where should I have my dinner?", 97 | listOf( 98 | PollOption("La Paradate - Barcelona"), 99 | PollOption("Ozzies Kokorec - Istanbul"), 100 | PollOption("Cafe Pushkin - Moscow"), 101 | PollOption("Gustarium - Florence"), 102 | PollOption("Donerci Mustafa - Berlin") 103 | ), 104 | "15:15 PM" 105 | ) 106 | ) 107 | recyclerViewAdapter.submitList(data) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /samples/src/main/java/me/ibrahimyilmaz/kiel/samples/listadapter/DiffExampleFragment.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.samples.listadapter 2 | 3 | import android.os.Bundle 4 | import android.view.ActionMode 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.Observer 9 | import androidx.recyclerview.widget.DividerItemDecoration 10 | import androidx.recyclerview.widget.RecyclerView 11 | import me.ibrahimyilmaz.kiel.adapter.RecyclerViewAdapter.Companion.adapterOf 12 | import me.ibrahimyilmaz.kiel.samples.R 13 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageListItemViewState 14 | import me.ibrahimyilmaz.kiel.samples.listadapter.model.MessageSelectionState 15 | import me.ibrahimyilmaz.kiel.samples.listadapter.viewholder.MessageViewHolder 16 | 17 | class DiffExampleFragment : 18 | Fragment(R.layout.fragment_diff_example), MessageActionModeCallbackListener { 19 | 20 | private val viewModel by viewModels() 21 | private lateinit var messageListRecyclerView: RecyclerView 22 | private var actionMode: ActionMode? = null 23 | 24 | private val actionModelCallback = 25 | MessageListItemSelectionActionModeCallback(this) 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | messageListRecyclerView = view.findViewById(R.id.messageListRecyclerView) 30 | messageListRecyclerView.adapter = messageListAdapter 31 | messageListRecyclerView.addItemDecoration( 32 | DividerItemDecoration( 33 | requireContext(), 34 | DividerItemDecoration.VERTICAL 35 | ) 36 | ) 37 | viewModel.messages.observe(viewLifecycleOwner, Observer(::onMessagesChanged)) 38 | viewModel.showDeleteAction.observe( 39 | viewLifecycleOwner, 40 | Observer(::onShowDeleteAction) 41 | ) 42 | 43 | viewModel.showSelectedMessageCount.observe( 44 | viewLifecycleOwner, 45 | Observer(::showSelectedMessageCount) 46 | ) 47 | } 48 | 49 | private fun showSelectedMessageCount(selectedMessageCount: Int) { 50 | actionMode?.title = "$selectedMessageCount" 51 | } 52 | 53 | private fun onMessagesChanged(messages: List) { 54 | messageListAdapter.submitList(messages) 55 | } 56 | 57 | private fun onShowDeleteAction(shouldShow: Boolean) { 58 | if (shouldShow) { 59 | if (actionMode == null) 60 | actionMode = requireActivity().startActionMode(actionModelCallback) 61 | } else { 62 | actionMode?.finish() 63 | } 64 | } 65 | 66 | private val messageListAdapter = adapterOf { 67 | diff( 68 | areContentsTheSame = { old, new -> old == new }, 69 | areItemsTheSame = { old, new -> old.message.id == new.message.id }, 70 | getChangePayload = { oldItem, newItem -> 71 | val diffBundle = Bundle() 72 | 73 | if (oldItem.selectionState != newItem.selectionState) { 74 | diffBundle.putParcelable( 75 | KEY_SELECTION, 76 | newItem.selectionState 77 | ) 78 | } 79 | 80 | if (diffBundle.isEmpty) null else diffBundle 81 | } 82 | ) 83 | register( 84 | layoutResource = R.layout.adapter_message_list_item, 85 | viewHolder = ::MessageViewHolder, 86 | onViewHolderCreated = { vh -> 87 | vh.itemView.setOnLongClickListener { 88 | val item = vh.boundItem ?: return@setOnLongClickListener false 89 | viewModel.onItemLongClicked(item) 90 | true 91 | } 92 | }, 93 | onBindBindViewHolder = { messageViewHolder, _, messageListItemViewState -> 94 | messageViewHolder.binding.senderText.text = messageListItemViewState.message.sender 95 | messageViewHolder.binding.contentText.text = 96 | messageListItemViewState.message.content 97 | messageViewHolder.binding.imageLetter.text = 98 | messageListItemViewState.message.sender.subSequence(0, 1) 99 | 100 | messageViewHolder.setSelectionState(messageListItemViewState.selectionState) 101 | }, 102 | onBindViewHolderWithPayload = { messageViewHolder, _, _, payloads -> 103 | (payloads.first() as? Bundle)?.getParcelable( 104 | KEY_SELECTION 105 | )?.let(messageViewHolder::setSelectionState) 106 | } 107 | ) 108 | } 109 | 110 | override fun onDeleteActionClicked() = viewModel.onDeleteActionClicked() 111 | 112 | override fun onActionModeDestroyed() { 113 | actionMode = null 114 | viewModel.onActionDeleteModeFinished() 115 | } 116 | 117 | private companion object { 118 | const val KEY_SELECTION = "KEY_SELECTION" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /kiel/src/test/java/me/ibrahimyilmaz/kiel/core/RecyclerViewHolderRendererTest.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import androidx.collection.SimpleArrayMap 4 | import com.google.common.truth.Truth.assertThat 5 | import com.nhaarman.mockitokotlin2.mock 6 | import com.nhaarman.mockitokotlin2.verify 7 | import org.junit.Before 8 | import org.junit.Test 9 | 10 | class RecyclerViewHolderRendererTest { 11 | 12 | private lateinit var itemTypes: SimpleArrayMap, Int> 13 | 14 | private lateinit var bindViewHolderListeners: 15 | SimpleArrayMap, OnBindViewHolder>> 16 | 17 | private lateinit var bindViewHolderWithPayloadListeners: 18 | SimpleArrayMap, OnBindViewHolderWithPayload>> 19 | 20 | private lateinit var renderer: RecyclerViewHolderRenderer> 21 | 22 | @Before 23 | fun setUp() { 24 | itemTypes = SimpleArrayMap() 25 | bindViewHolderListeners = SimpleArrayMap() 26 | bindViewHolderWithPayloadListeners = SimpleArrayMap() 27 | } 28 | 29 | @Test 30 | fun `Should return item view type for the given item`() { 31 | // GIVEN 32 | val expectedItemViewType = 1 33 | setUpItemTypes(Any(), expectedItemViewType) 34 | 35 | renderer = 36 | RecyclerViewHolderRenderer( 37 | itemTypes, 38 | bindViewHolderListeners, 39 | bindViewHolderWithPayloadListeners 40 | ) 41 | // WHEN 42 | val actualItemViewType = renderer.getItemViewType(Any()) 43 | 44 | // THEN 45 | assertThat(actualItemViewType).isEqualTo(expectedItemViewType) 46 | } 47 | 48 | @Test 49 | fun `Should render viewHolder with the given viewHolder and item`() { 50 | // GIVEN 51 | renderer = 52 | RecyclerViewHolderRenderer( 53 | itemTypes, 54 | bindViewHolderListeners, 55 | bindViewHolderWithPayloadListeners 56 | ) 57 | val viewHolder = mock>() 58 | val position = 1 59 | val item = Any() 60 | 61 | // WHEN 62 | renderer.renderViewHolder(viewHolder, position, item) 63 | 64 | // THEN 65 | verify(viewHolder).bind(position, item) 66 | } 67 | 68 | @Test 69 | fun `Should notify the ViewHolderBound listener`() { 70 | // GIVEN 71 | val onBoundListener = mock>>() 72 | setUpViewHolderBoundListeners(Any(), onBoundListener) 73 | renderer = 74 | RecyclerViewHolderRenderer( 75 | itemTypes, 76 | bindViewHolderListeners, 77 | bindViewHolderWithPayloadListeners 78 | ) 79 | val viewHolder = mock>() 80 | val position = 1 81 | val item = Any() 82 | 83 | // WHEN 84 | renderer.renderViewHolder(viewHolder, position, item) 85 | 86 | // THEN 87 | verify(onBoundListener).invoke(viewHolder, position, item) 88 | } 89 | 90 | @Test 91 | fun `Should render viewHolder with the given viewHolder, item and payload`() { 92 | // GIVEN 93 | renderer = 94 | RecyclerViewHolderRenderer( 95 | itemTypes, 96 | bindViewHolderListeners, 97 | bindViewHolderWithPayloadListeners 98 | ) 99 | val viewHolder = mock>() 100 | val position = 1 101 | val item = Any() 102 | val payloads = mock>() 103 | 104 | // WHEN 105 | renderer.renderViewHolder(viewHolder, position, item, payloads) 106 | 107 | // THEN 108 | verify(viewHolder).bind(position, item, payloads) 109 | } 110 | 111 | @Test 112 | fun `Should notify onBindViewHolderWithPayload listener`() { 113 | // GIVEN 114 | val onBoundListener = mock>>() 115 | setUpViewHolderBoundWithPayloadListeners(Any(), onBoundListener) 116 | renderer = 117 | RecyclerViewHolderRenderer( 118 | itemTypes, 119 | bindViewHolderListeners, 120 | bindViewHolderWithPayloadListeners 121 | ) 122 | val viewHolder = mock>() 123 | val position = 1 124 | val item = Any() 125 | val payloads = mock>() 126 | 127 | // WHEN 128 | renderer.renderViewHolder(viewHolder, position, item, payloads) 129 | 130 | // THEN 131 | verify(onBoundListener).invoke(viewHolder, position, item, payloads) 132 | } 133 | 134 | private fun setUpViewHolderBoundListeners( 135 | item: Any, 136 | listener: OnBindViewHolder> 137 | ) { 138 | bindViewHolderListeners.put(item.javaClass, listener) 139 | } 140 | 141 | private fun setUpViewHolderBoundWithPayloadListeners( 142 | item: Any, 143 | listener: OnBindViewHolderWithPayload> 144 | ) { 145 | bindViewHolderWithPayloadListeners.put(item.javaClass, listener) 146 | } 147 | 148 | private fun setUpItemTypes(item: Any, itemViewType: Int) { 149 | itemTypes.put(item.javaClass, itemViewType) 150 | } 151 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/ibrahimyilmaz/kiel/workflows/build/badge.svg) 2 | [ ![Download](https://api.bintray.com/packages/ibrahimyilmaz/kiel/kiel/images/download.svg) ](https://bintray.com/ibrahimyilmaz/kiel/kiel/_latestVersion) 3 | 4 | ## Kiel 5 | 6 | Kiel is a `RecyclerView.Adapter` with a minimalist and convenient Kotlin DSL which provides utility on top of Android's normal `RecyclerView.Adapter`. 7 | 8 | kiel_icon 9 | 10 | Most of time: 11 | - We found ourselves repeating same boiler plate codes for `RecyclerView.Adapter`. 12 | - We have difficulty in handling `RecyclerView.Adapter` when there are many `viewTypes`. 13 | 14 | But now, Kiel may help us to get rid of these problems. 15 | 16 | ## Usage: 17 | 18 | #### Basic Usage: 19 | 20 | ```kt 21 | val recyclerViewAdapter = adapterOf { 22 | register( 23 | layoutResource = R.layout.adapter_message_text_item, 24 | viewHolder = ::TextMessageViewHolder, 25 | onViewHolderCreated = { vh-> 26 | //you may handle your on click listener 27 | vh.itemView.setOnClickListener { 28 | 29 | } 30 | }, 31 | onBindViewHolder = { vh, _, it -> 32 | vh.messageText.text = it.text 33 | vh.sentAt.text = it.sentAt 34 | } 35 | ) 36 | } 37 | 38 | recyclerView.adapter = recyclerViewAdapter 39 | ``` 40 | 41 | #### Different View Types: 42 | 43 | You may register different `ViewHolder`s. 44 | 45 | ```kt 46 | val recyclerViewAdapter = adapterOf { 47 | register( 48 | layoutResource = R.layout.adapter_message_text_item, 49 | viewHolder = ::TextMessageViewHolder, 50 | onBindViewHolder = { vh, _, it -> 51 | vh.messageText.text = it.text 52 | vh.sentAt.text = it.sentAt 53 | } 54 | ) 55 | 56 | register( 57 | layoutResource = R.layout.adapter_message_image_item, 58 | viewHolder = ::ImageMessageViewHolder, 59 | onBindViewHolder = { vh, _, item -> 60 | vh.messageText.text = item.text 61 | vh.sentAt.text = item.sentAt 62 | 63 | Glide.with(vh.messageImage) 64 | .load(item.imageUrl) 65 | .into(vh.messageImage) 66 | } 67 | ) 68 | } 69 | 70 | 71 | recyclerView.adapter = recyclerViewAdapter 72 | ``` 73 | #### Handling Events: 74 | 75 | As `ViewHolder` instance is accessible in: 76 | - `onViewHolderCreated` 77 | - `onBindViewHolder` 78 | - `onBindViewHolderWithPayload` 79 | 80 | 81 | You can handle the events in the same way how you did it before. 82 | ```kt 83 | val recyclerViewAdapter = adapterOf { 84 | register( 85 | layoutResource = R.layout.adapter_message_text_it, 86 | viewHolder = ::TextMessageViewHolder, 87 | onViewHolderCreated = { vh-> 88 | vh.itemView.setOnClickListener { 89 | 90 | } 91 | vh.messageText.addTextChangedListener{text -> 92 | 93 | } 94 | }, 95 | onBindViewHolder = { vh, _, it -> 96 | vh.messageText.text = it.text 97 | vh.sentAt.text = it.sentAt 98 | } 99 | ) 100 | } 101 | 102 | recyclerView.adapter = recyclerViewAdapter 103 | ``` 104 | 105 | 106 | #### DiffUtil: 107 | 108 | You may provide your custom `DiffUtil.ItemCallback` by extending `RecyclerDiffUtilCallback` with `diffUtilCallback`: 109 | 110 | ```kt 111 | val recyclerViewAdapter = adapterOf { 112 | diff( 113 | areContentsTheSame = { old, new -> old == new }, 114 | areItemsTheSame = { old, new -> old.message.id == new.message.id }, 115 | getChangePayload = { oldItem, newItem -> 116 | val diffBundle = Bundle() 117 | 118 | if (oldItem.selectionState != newItem.selectionState) { 119 | diffBundle.putParcelable( 120 | TextMessageViewHolder.KEY_SELECTION, 121 | newItem.selectionState 122 | ) 123 | } 124 | 125 | if (diffBundle.isEmpty) null else diffBundle 126 | } 127 | ) 128 | register ( 129 | layoutResource = R.layout.adapter_message_text_item, 130 | viewHolder = ::TextMessageViewHolder, 131 | onBindViewHolder = { vh, _, it -> 132 | vh.messageText.text = it.message.text 133 | vh.sentAt.text = it.message.sentAt 134 | } 135 | ) 136 | 137 | ``` 138 | Download 139 | -------- 140 | 141 | ```groovy 142 | implementation 'me.ibrahimyilmaz:kiel:latestVersion' 143 | ``` 144 | 145 | License 146 | ------- 147 | ``` 148 | Copyright 2020 Ibrahim Yilmaz 149 | 150 | Licensed under the Apache License, Version 2.0 (the "License"); 151 | you may not use this file except in compliance with the License. 152 | You may obtain a copy of the License at 153 | 154 | http://www.apache.org/licenses/LICENSE-2.0 155 | 156 | Unless required by applicable law or agreed to in writing, software 157 | distributed under the License is distributed on an "AS IS" BASIS, 158 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 159 | See the License for the specific language governing permissions and 160 | limitations under the License. 161 | ``` 162 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /kiel/src/main/java/me/ibrahimyilmaz/kiel/core/AdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.View 4 | import androidx.annotation.LayoutRes 5 | import androidx.collection.SimpleArrayMap 6 | import androidx.recyclerview.widget.RecyclerView.Adapter 7 | 8 | abstract class AdapterFactory>> { 9 | internal val viewHolderMap = 10 | SimpleArrayMap>>() 11 | 12 | internal val layoutResourceMap = 13 | SimpleArrayMap, Int>() 14 | 15 | internal val viewHolderCreatedListeners = 16 | SimpleArrayMap>>() 17 | 18 | internal val onBindViewHolderListeners = 19 | SimpleArrayMap, OnBindViewHolder>>() 20 | 21 | internal val onBindViewHolderWithPayloadListeners = 22 | SimpleArrayMap, OnBindViewHolderWithPayload>>() 23 | 24 | fun registerLayoutResource( 25 | type: Class<*>, 26 | layoutResource: Int 27 | ) { 28 | layoutResourceMap.put(type, layoutResource) 29 | } 30 | 31 | fun registerViewHolderFactory( 32 | layoutResource: Int, 33 | viewHolderFactory: ViewHolderFactory> 34 | ) { 35 | viewHolderMap.put( 36 | layoutResource, 37 | viewHolderFactory 38 | ) 39 | } 40 | 41 | fun registerViewHolderCreatedListener( 42 | layoutResource: Int, 43 | value: OnViewHolderCreated> 44 | ) { 45 | viewHolderCreatedListeners.put( 46 | layoutResource, 47 | value 48 | ) 49 | } 50 | 51 | fun registerViewHolderBoundListener( 52 | type: Class<*>, 53 | value: OnBindViewHolder> 54 | ) { 55 | onBindViewHolderListeners.put( 56 | type, 57 | value 58 | ) 59 | } 60 | 61 | fun registerViewHolderBoundWithPayloadListener( 62 | type: Class<*>, 63 | value: OnBindViewHolderWithPayload> 64 | ) { 65 | onBindViewHolderWithPayloadListeners.put( 66 | type, 67 | value 68 | ) 69 | } 70 | 71 | @Deprecated( 72 | "In general we try to avoid builder classes in Kotlin," + 73 | " as they're not needed and introduce nullability." + 74 | " So AdapterRegistryBuilder will be retired soon", 75 | ReplaceWith( 76 | "register( " + 77 | "viewHolder = ," + 78 | "layoutResource = ," + 79 | "onViewHolderCreated = ," + 80 | "onViewHolderBound = ," + 81 | "onViewHolderBoundWithPayload = " + 82 | ")" 83 | ) 84 | ) 85 | fun register( 86 | lambda: AdapterRegistryBuilder.() -> Unit 87 | ) { 88 | with( 89 | AdapterRegistryBuilder() 90 | .apply(lambda).build() 91 | ) { 92 | viewHolderMap.put( 93 | layoutResource, 94 | viewHolderFactory 95 | ) 96 | layoutResourceMap.put(type, layoutResource) 97 | 98 | onViewHolderCreated?.let { 99 | registerViewHolderCreatedListener(layoutResource, it) 100 | } 101 | 102 | onBindViewHolder?.let { 103 | onBindViewHolderListeners.put(type, it) 104 | } 105 | 106 | onViewHolderBoundWithPayload?.let { 107 | onBindViewHolderWithPayloadListeners.put(type, it) 108 | } 109 | } 110 | } 111 | 112 | inline fun > register( 113 | crossinline viewHolder: ViewHolderCreator, 114 | @LayoutRes 115 | layoutResource: Int, 116 | noinline onViewHolderCreated: OnViewHolderCreated? = null, 117 | noinline onBindBindViewHolder: OnBindViewHolder? = null, 118 | noinline onBindViewHolderWithPayload: OnBindViewHolderWithPayload? = null 119 | ) { 120 | val itemType = P::class.java 121 | 122 | val viewHolderFactory = object : ViewHolderFactory { 123 | override fun instantiate(view: View): VH = viewHolder(view) 124 | } 125 | 126 | registerViewHolderFactory(layoutResource, viewHolderFactory) 127 | 128 | registerLayoutResource(itemType, layoutResource) 129 | 130 | onViewHolderCreated?.let { 131 | val onViewHolderCreatedListener = object : OnViewHolderCreated> { 132 | override fun invoke(viewHolder: RecyclerViewHolder) = 133 | it(viewHolder as VH) 134 | } 135 | 136 | registerViewHolderCreatedListener(layoutResource, onViewHolderCreatedListener) 137 | } 138 | 139 | onBindBindViewHolder?.let { 140 | val onBindViewHolderListener = object : OnBindViewHolder> { 141 | override fun invoke(viewHolder: RecyclerViewHolder, position: Int, item: T) = 142 | it(viewHolder as VH, position, item as P) 143 | } 144 | 145 | registerViewHolderBoundListener(itemType, onBindViewHolderListener) 146 | } 147 | 148 | onBindViewHolderWithPayload?.let { 149 | val onBindViewHolderWithPayloadListener = 150 | object : OnBindViewHolderWithPayload> { 151 | override fun invoke( 152 | viewHolder: RecyclerViewHolder, 153 | position: Int, 154 | item: T, 155 | payloads: List 156 | ) = it(viewHolder as VH, position, item as P, payloads) 157 | } 158 | 159 | registerViewHolderBoundWithPayloadListener( 160 | itemType, 161 | onBindViewHolderWithPayloadListener 162 | ) 163 | } 164 | } 165 | 166 | abstract fun create(): A 167 | } -------------------------------------------------------------------------------- /kiel/src/test/java/me/ibrahimyilmaz/kiel/core/AdapterRegistryBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package me.ibrahimyilmaz.kiel.core 2 | 3 | import android.view.View 4 | import com.google.common.truth.Truth.assertThat 5 | import com.nhaarman.mockitokotlin2.mock 6 | import me.ibrahimyilmaz.kiel.utils.TestRecyclerViewHolder 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.mockito.junit.MockitoJUnitRunner 10 | 11 | @RunWith(MockitoJUnitRunner::class) 12 | class AdapterRegistryBuilderTest { 13 | 14 | @Test(expected = IllegalArgumentException::class) 15 | fun `Should throw Illegal Argument Exception when view type is missing`() { 16 | // GIVEN 17 | val recyclerViewAdapterRegistryBuilder = 18 | AdapterRegistryBuilder().apply { 19 | layoutResource { 1 } 20 | viewHolder(::TestRecyclerViewHolder) 21 | } 22 | 23 | // WHEN 24 | recyclerViewAdapterRegistryBuilder.build() 25 | } 26 | 27 | @Test(expected = IllegalArgumentException::class) 28 | fun `Should throw Illegal Argument Exception when layout resource is missing`() { 29 | // GIVEN 30 | val recyclerViewAdapterRegistryBuilder = 31 | AdapterRegistryBuilder().apply { 32 | type { Any::class.java } 33 | viewHolder(::TestRecyclerViewHolder) 34 | } 35 | 36 | // WHEN 37 | recyclerViewAdapterRegistryBuilder.build() 38 | } 39 | 40 | @Test(expected = IllegalArgumentException::class) 41 | fun `Should throw Illegal Argument Exception when viewHolder constructor is missing`() { 42 | // GIVEN 43 | val recyclerViewAdapterRegistryBuilder = 44 | AdapterRegistryBuilder().apply { 45 | type { Any::class.java } 46 | layoutResource { 1 } 47 | } 48 | 49 | // WHEN 50 | recyclerViewAdapterRegistryBuilder.build() 51 | } 52 | 53 | @Test 54 | fun `Should build RecyclerViewAdapterRegistry`() { 55 | // GIVEN 56 | val expectedRecyclerViewAdapterRegistry = 57 | RecyclerViewAdapterRegistry( 58 | Any::class.java, 59 | object : ViewHolderFactory> { 60 | override fun instantiate(view: View): RecyclerViewHolder = 61 | TestRecyclerViewHolder(view) 62 | }, 63 | 1 64 | ) 65 | 66 | val recyclerViewAdapterRegistryBuilder = 67 | AdapterRegistryBuilder().apply { 68 | type { Any::class.java } 69 | layoutResource { 1 } 70 | viewHolder(::TestRecyclerViewHolder) 71 | } 72 | 73 | // WHEN 74 | val actualRecyclerViewAdapterRegistry = recyclerViewAdapterRegistryBuilder.build() 75 | 76 | // THEN 77 | assertRecyclerViewAdapterRegistry( 78 | actualRecyclerViewAdapterRegistry, 79 | expectedRecyclerViewAdapterRegistry 80 | ) 81 | } 82 | 83 | @Test 84 | fun `Should build RecyclerViewAdapterRegistry with OnViewHolderCreated`() { 85 | // GIVEN 86 | val onViewHolderCreatedListener = mock>() 87 | 88 | val recyclerViewAdapterRegistryBuilder = 89 | AdapterRegistryBuilder().apply { 90 | type { Any::class.java } 91 | layoutResource { 1 } 92 | viewHolder(::TestRecyclerViewHolder) 93 | onViewHolderCreated(onViewHolderCreatedListener) 94 | } 95 | 96 | // WHEN 97 | val actualRecyclerViewAdapterRegistry = recyclerViewAdapterRegistryBuilder.build() 98 | 99 | // THEN 100 | assertThat(actualRecyclerViewAdapterRegistry.onViewHolderCreated).isNotNull() 101 | } 102 | 103 | @Test 104 | fun `Should build RecyclerViewAdapterRegistry with OnViewHolderBound`() { 105 | // GIVEN 106 | val onViewHolderBoundListener = mock>() 107 | 108 | val recyclerViewAdapterRegistryBuilder = 109 | AdapterRegistryBuilder().apply { 110 | type { Any::class.java } 111 | layoutResource { 1 } 112 | viewHolder(::TestRecyclerViewHolder) 113 | onViewHolderBound(onViewHolderBoundListener) 114 | } 115 | 116 | // WHEN 117 | val actualRecyclerViewAdapterRegistry = recyclerViewAdapterRegistryBuilder.build() 118 | 119 | // THEN 120 | assertThat(actualRecyclerViewAdapterRegistry.onBindViewHolder).isNotNull() 121 | } 122 | 123 | @Test 124 | fun `Should build RecyclerViewAdapterRegistry with OnViewHolderBoundWithPayload`() { 125 | // GIVEN 126 | val onViewHolderBoundWithPayloadListener = 127 | mock>() 128 | 129 | val recyclerViewAdapterRegistryBuilder = 130 | AdapterRegistryBuilder().apply { 131 | type { Any::class.java } 132 | layoutResource { 1 } 133 | viewHolder(::TestRecyclerViewHolder) 134 | onViewHolderBoundWithPayload(onViewHolderBoundWithPayloadListener) 135 | } 136 | 137 | // WHEN 138 | val actualRecyclerViewAdapterRegistry = recyclerViewAdapterRegistryBuilder.build() 139 | 140 | // THEN 141 | assertThat(actualRecyclerViewAdapterRegistry.onViewHolderBoundWithPayload).isNotNull() 142 | } 143 | 144 | private fun > assertRecyclerViewAdapterRegistry( 145 | actualRecyclerViewAdapterRegistry: RecyclerViewAdapterRegistry, 146 | expectedRecyclerViewAdapterRegistry: RecyclerViewAdapterRegistry 147 | ) { 148 | assertThat(actualRecyclerViewAdapterRegistry.type).isEqualTo( 149 | expectedRecyclerViewAdapterRegistry.type 150 | ) 151 | assertThat(actualRecyclerViewAdapterRegistry.layoutResource).isEqualTo( 152 | expectedRecyclerViewAdapterRegistry.layoutResource 153 | ) 154 | } 155 | } -------------------------------------------------------------------------------- /samples/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | --------------------------------------------------------------------------------