Kotlin RecyclerView in a Proper and re-usable way

Anik Dey
5 min readApr 20, 2019

Today i am going to share something that is very common and for sure you will find dozens of implementation of it, recyclerview in kotlin.

Actually i was trying to show a list using recyclerview in kotlin, searched on the internet. The code sample i came across was not very good and i implemented the recyclerview using generics. Another important thing is that whenever we are creating an adapter for a recyclerview, we implement the event listener for views in the adapter which is not a good practice. The adapters sole responsibility is to only show data.

But if you are handling the events (for example starting a new activity from your adapter based on click event of a view) you are violating the Single Responsibility Principle (SRP) of SOLID and your adapter will be messed up.

I am going to be very naive with my project structure. So let us start coding. The source code is available here

Add these dependencies in your app.gradle and perform a sync.

dependencies {
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity"
>

<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

</LinearLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(savedInstanceState == null) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_container, ListFragment.newInstance(), ListFragment.TAG).commit();
}
}
}

fragment_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
>

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

</LinearLayout>

ListFragment.kt

class ListFragment: Fragment() {

protected lateinit var rootView: View
lateinit var recyclerView: RecyclerView

companion object {
var TAG = ListFragment::class.java.simpleName
const val ARG_POSITION: String = "positioin"

fun
newInstance(): ListFragment {
var fragment = ListFragment();
val args = Bundle()
args.putInt(ARG_POSITION, 1)
fragment.arguments = args
return fragment
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateComponent()
}

private fun onCreateComponent() {

}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
rootView = inflater.inflate(R.layout.fragment_list, container, false);
initView()
return rootView
}

private fun initView(){
initializeRecyclerView()
}

private fun initializeRecyclerView() {
recyclerView = rootView.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(activity)
//recyclerView.adapter = adapter
}
}

Let me explain some code of the fragment above.

val args = Bundle()
args.putInt(ARG_POSITION, 1)
fragment.arguments = args

The above three lines of code has nothing to do with this project, just to show you how you should pass values to your fragment using Bundle() i added this code here.

private fun onCreateComponent() {

}

Inside this method i will create instances of classes. I will create instance of my adapter later inside this method so i called this method inside the onCreate(savedInstanceState: Bundle?) method to make sure my adapter is not null.

private fun initializeRecyclerView() {
recyclerView = rootView.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(activity)
//recyclerView.adapter = adapter
}

Here i am initializing my recyclerview. I commented the line

//recyclerView.adapter = adapter

for now as we haven’t yet created our adapter. Now we will move forward to create our adapter. Before that we are going to create an interface like that

interface OnItemClickListener {
abstract fun onItemClick(position: Int, view: View?)
}

Now we are going to create our base class for adapter.

abstract class BaseRecyclerViewAdapter<T>:  RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var list: ArrayList<T>? = ArrayList<T>()
protected var itemClickListener: OnItemClickListener? = null

fun addItems(items: ArrayList<T>) {
this.list?.addAll(items)
reload()
}

fun clear() {
this.list?.clear()
reload()
}

fun getItem(position: Int): T? {
return this.list?.get(position)
}

fun setOnItemClickListener(onItemClickListener: OnItemClickListener) {
this.itemClickListener = onItemClickListener
}

override fun getItemCount(): Int = list!!.size

private fun
reload() {
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
}
}

I have created a base class for my adapter with a generic type parameter T.

The type T is indicating the type for which i am going to create an adapter. For example i am going to create a ListAdapter to show users. So this type parameter T will be replaced with User model. With our BaseRecyclerViewAdapter ready let us create our ListAdapter now.

class ListAdapter: BaseRecyclerViewAdapter<User>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return MyViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.list_item, parent, false))
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
var myHolder = holder as? MyViewHolder
myHolder?.setUpView(user = getItem(position))
}

inner class MyViewHolder (view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {

private val imageView: ImageView = view.image_view
private val textView: TextView = view.text_view

init {
view.setOnClickListener(this)
}

fun setUpView(user: User?) {
user?.resId?.let { imageView.setImageResource(it) }
textView
.text = user?.name
}

override fun onClick(v: View?) {
itemClickListener?.onItemClick(adapterPosition, v)
}
}
}

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
>

<ImageView
android:id="@+id/image_view"
android:layout_width="50dp"
android:layout_height="50dp"
/>

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
/>

</LinearLayout>

User.kt

import android.os.Parcel
import android.os.Parcelable

data class User(val name: String, val resId: Int) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readInt()
)

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeInt(resId)
}

override fun describeContents(): Int {
return 0
}

companion object CREATOR : Parcelable.Creator<User> {
override fun createFromParcel(parcel: Parcel): User {
return User(parcel)
}

override fun newArray(size: Int): Array<User?> {
return arrayOfNulls(size)
}
}
}

Now let us update our fragment class like with the following codes.

protected lateinit var rootView: View
lateinit var recyclerView: RecyclerView
lateinit var adapter: ListAdapter

in onCreateComponent()

private fun onCreateComponent() {
adapter = ListAdapter()
}

add the following methods and call them inside initView()

private fun initView(){
setUpAdapter()
initializeRecyclerView()
setUpDummyData()
}

private fun setUpAdapter() {
adapter.setOnItemClickListener(onItemClickListener = object : OnItemClickListener {
override fun onItemClick(position: Int, view: View?) {
var user = adapter.getItem(position)
startActivity(context?.let {ctx ->
user?.let {
user -> DetailsActivity.newIntent(ctx, user)
}
}
)
}
})
}

private fun initializeRecyclerView() {
recyclerView = rootView.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.adapter = adapter
}

private fun setUpDummyData(){
var list: ArrayList<User> = ArrayList<User>()
list.add(User("User 1", R.drawable.user))
list.add(User("User 2", R.drawable.user))
list.add(User("User 3", R.drawable.user))
list.add(User("User 4", R.drawable.user))
list.add(User("User 5", R.drawable.user))
list.add(User("User 6", R.drawable.user))
list.add(User("User 7", R.drawable.user))
list.add(User("User 8", R.drawable.user))
list.add(User("User 9", R.drawable.user))
adapter.addItems(list)
}

Let us create our details activity

activity_details.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.DetailsActivity"
>

<ImageView
android:id="@+id/image_view"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
android:layout_marginBottom="16dp"
android:layout_centerHorizontal="true"
/>
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="24sp"
android:layout_centerHorizontal="true"
android:layout_below="@id/image_view"
/>
</RelativeLayout>

DetailsActivity.kt

class DetailsActivity : AppCompatActivity() {

private lateinit var imageView: ImageView
private lateinit var textView: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details)
val user: User = intent.getParcelableExtra(EXTRA_USER_MODEL)
supportActionBar?.title = user.name

imageView
= findViewById(R.id.image_view)
textView = findViewById(R.id.text_view)

imageView.setImageResource(user?.resId)
textView.setText("Showing information of "+ user?.name)

}

companion object {
var TAG = DetailsActivity::class.java.simpleName
const val EXTRA_USER_MODEL: String = "user"

fun
newIntent(context: Context, user: User): Intent {
var intent = Intent(context, DetailsActivity::class.java)
intent.putExtra(EXTRA_USER_MODEL, user)
return intent
}
}
}

--

--