Skip to content

An easy way to define list adapters without boilerplate code for both adapters of one item and multi-item adapters

Notifications You must be signed in to change notification settings

ElianFabian/SimpleListAdapter

Repository files navigation

SimpleListAdapter

The README may not be updated.

This repository offers a simple method to create list adapters in Kotlin without the need for excessive code. It is important to note that this is not a library, but rather a demonstration of using specific classes and functions to simplify the adapter creation process. While this approach may not address every scenario, it can help you implement straightforward concepts in a clean and fast manner. As mentioned since this isn't a library that provides a certain workflow to define adapters feel free to modify the code to fit your particular requirements.

To better understand the advantages of this approach, the repository includes a simple app with 3 screens:

  • SingleItemAdapter
  • Nested adapters
  • MultiItemAdapter

In the file with the adapter definitions you have a the same adapter twice, one applying the method proposed here and the regular one to allow an easy comparison.

The primary focus of this simplification is on the binding aspect of adapters, which is typically the most important aspect of adapter creation.

Setup

This method only requieres the ViewBinding feature in the build.gradle app:

buildFeatures {  
    viewBinding true  
}

Then add these files to your project:

SingleItemListAdapter

MultiItemListAdapter

Usage

SingleItemAdapter

Let's define a simple single-item type adapter! In this case, we will create an adapter for simple arithmetic operations.

We have this data class for the item:

data class OperationInfo(  
    val firstNumber: Int,  
    val secondNumber: Int,  
    val operationSymbol: String,  
    val result: Int,  
    val uuid: String = UUID.randomUUID().toString(),  
)

Next, we have the ** XML** layout that we will use for the item. We won't show the XML code here to keep it simple:item_layout

Now, let's create our adapter. We'll start with a simple version:

@Suppress("FunctionName")
fun OperationAdapter() = SimpleListAdapter(
    inflate = ItemOperationBinding::inflate,
    areItemsTheSame = { oldItem, newItem -> oldItem.uuid == newItem.uuid },
) { binding, operation: OperationInfo, position ->

    binding.apply {
        tvFirstNumber.text = "${operation.firstNumber}"
        tvSecondNumber.text = "${operation.secondNumber}"
        tvOperationSymbol.text = operation.operationSymbol
        tvExpectedResult.text = "${operation.result}"
    }
}

And there you have it! A list adapter that's ready to use. Let's explain the rest of the details:

As you can see, we're defining a function to create the adapter. Since it's meant to be used like a regular adapter, we'll use PascalCase naming convention (but of course, you can name it however you prefer).

We then indicate which *Binding class we're going to use by passing the inflate method as a reference. In the lambda block, we define the binding logic.

We also need to specify which item class we want to use. We could do this by using type parameters, but I think it's more elegant to put it in the lambda parameter.

Now let's say we want to be able to modify our list without any problem, pass an onItemClick lambda and a list when creating an instance of our adapter:

@Suppress("FunctionName")
fun OperationAdapter(
    items: List<OperationInfo>,
    onItemClick: (operation: OperationInfo) -> Unit,
) = SimpleListAdapter(
    inflate = ItemOperationBinding::inflate,
    areItemsTheSame = { oldItem, newItem -> oldItem.uuid == newItem.uuid },
) { binding, operation: OperationInfo, position ->

    binding.apply {
        tvFirstNumber.text = "${operation.firstNumber}"
        tvSecondNumber.text = "${operation.secondNumber}"
        tvOperationSymbol.text = operation.operationSymbol
        tvExpectedResult.text = "${operation.result}"
    }

    binding.root.setOnClickListener { onItemClick(operation) }

}.apply { submitList(items) }

Now we're done! We can use it like any regular list adapter. By the way, you can also give a value to the areContentsTheSame parameter, but most of the time, it wouldn't be necessary since its default value is:

areContentsTheSame = { oldItem, newItem -> oldItem == newItem }

It's also worth mentioning that the scope of the lambda is the adapter's one, so you can access its functions without any problem. However, since getItem() is marked as protected we've defined several extension functions inside the custom adapter class for getItem() and getItemOrNull() to use when needed. Also, since the return type of the SimpleListAdapter function is just ListAdapter, you won't be able to access those extension functions outside of the lambda to respect the original restriction of the getItem function in the ListAdapter class.

Now we could use it like this in an Activity or a Fragment:

val operationAdapter = OperationAdapter_New(
    items = listOfOperation,
    onItemClick = { operation ->

        val message = operation.run { "$firstNumber $operationSymbol $secondNumber = $result" }

        Toast.makeText(
            applicationContext,
            message,
            Toast.LENGTH_SHORT,
        ).show()
    },
)

binding.recyclerView.adapter = operationAdapter

Feel free to check this file to compare this solution to the regular one.

Also if you want to see how to do the same thing with nested adapters check this one.


MultiItemAdapter

Let's now define a simple multi-item type adapter! It's actually quite similar to what we have seen so far, but instead of a binding block, we have a list of binding blocks.

For this adapter, we will be creating a chat message display. Here are our sealed data classes:

sealed class Message {
    val uuid: String = UUID.randomUUID().toString()
}

data class UserMessage(
    val content: String,
    val hour: String,
) : Message()

data class OtherUserMessage(
    val senderName: String,
    val content: String,
    val hour: String,
) : Message()

The own user message layout:

The other user message layout:

Now, we are ready to define our adapter:

@Suppress("FunctionName")
fun MessagesAdapter(
    messages: List<Message>,
    onUserMessageClick: (message: UserMessage) -> Unit,
    onOtherUserMessageClick: (message: OtherUserMessage) -> Unit,
) = SimpleListAdapter(
    areItemsTheSame = { oldItem, newItem -> oldItem.uuid == newItem.uuid },
    itemBindings = listOf(
        Binding(ItemUserMessageBinding::inflate) { binding, message: UserMessage, position ->

            binding.apply {
                tvContent.text = message.content
                tvTime.text = message.hour
            }

            binding.root.setOnClickListener { onUserMessageClick(message) }
        },
        Binding(ItemOtherUserMessageBinding::inflate) { binding, message: OtherUserMessage, position ->

            binding.apply {
                tvSenderName.text = message.senderName
                tvContent.text = message.content
                tvTime.text = message.hour
            }

            binding.root.setOnClickListener { onOtherUserMessageClick(message) }
        },
    ),
).apply { submitList(messages) }

In this case to specify the bindings we make use of the Binding() function, what it simply does is to return the data needed for the adapter to know how to bind any item type to its view.

And there it is, a simple, clean, and ready-to-use multi-item adapter! Feel free to check out this file to compare this solution to the regular one.