Challenge: RecyclerView ViewTypes

Hi! After much head-scratching this is the solution I came up with to this challenge. I created a simple second layout identical to the original except for an added “Contact Police” button under the root LinearLayout. Then I assigned “requiresPolice” (Boolean) randomly to each item in the dummy list in CrimeListViewModel. I would be very interested in any suggestions and improvements…

CrimeListViewModel.kt:

 package com.bignerdranch.android.criminalintent

    import androidx.lifecycle.ViewModel

    class CrimeListViewModel : ViewModel() {

        val crimes = mutableListOf<Crime>()

        init {
            for (i in 0 until 100) {
                val crime = Crime()
                crime.title = "Crime #$i"
                crime.isSolved = i % 2 == 0
                crime.requiresPolice = when ((0..1).shuffled().first()) {
                    0 -> false
                    else -> true
                }
                crimes += crime
            }
        }
    }

Here is CrimeListFragment.kt:

package com.bignerdranch.android.criminalintent

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

private const val TAG = "CrimeListFragment"

class CrimeListFragment : Fragment() {

    private lateinit var crimeRecyclerView: RecyclerView
    private var adapter: CrimeAdapter? = null

    private val crimeListViewModel: CrimeListViewModel by lazy {
        ViewModelProviders.of(this).get(CrimeListViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "Total crimes: ${crimeListViewModel.crimes.size}")
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime_list, container, false)

        crimeRecyclerView = view.findViewById(R.id.crime_recycler_view) as RecyclerView
        crimeRecyclerView.layoutManager = LinearLayoutManager(context)

        updateUI()

        return view
    }

    private fun updateUI() {
        val crimes = crimeListViewModel.crimes
        adapter = CrimeAdapter(crimes)
        crimeRecyclerView.adapter = adapter
    }

    companion object {
        fun newInstance(): CrimeListFragment {
            return CrimeListFragment()
        }
    }

    private abstract class CrimeHolder(view: View) : RecyclerView.ViewHolder(view) {
        var crime = Crime()
        val titleTextView: TextView = itemView.findViewById(R.id.crime_title)
        val dateTextView: TextView = itemView.findViewById(R.id.crime_date)
    }

    private inner class NormalCrimeHolder(view: View) : CrimeHolder(view), View.OnClickListener {

        init {
            itemView.setOnClickListener(this)
        }

        fun bind(crime: Crime) {
            this.crime = crime
            titleTextView.text = this.crime.title
            dateTextView.text = this.crime.date.toString()
        }

        override fun onClick(v: View) {
            Toast
                .makeText(context, "${crime.title} pressed!", Toast.LENGTH_SHORT)
                .show()
        }
    }

    private inner class SeriousCrimeHolder(view: View) : CrimeHolder(view), View.OnClickListener {
        val contactPoliceButton: Button = itemView.findViewById(R.id.contact_police_button)

        init {
            itemView.setOnClickListener(this)
        }

        fun bind(crime: Crime) {
            this.crime = crime
            titleTextView.text = this.crime.title
            dateTextView.text = this.crime.date.toString()
            contactPoliceButton.setOnClickListener {
                Toast.makeText(context, "This crime is serious!", Toast. LENGTH_SHORT).show()
            }
        }

        override fun onClick(v: View) {
            Toast
                .makeText(context, "${crime.title} pressed!", Toast.LENGTH_SHORT)
                .show()
        }
    }

    private inner class CrimeAdapter(var crimes: List<Crime>)
        : RecyclerView.Adapter<CrimeHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CrimeHolder {
            return when (viewType) {
                0 -> {
                    val view = layoutInflater.inflate(R.layout.list_item_crime, parent, false)
                    NormalCrimeHolder(view)
                }
                else -> {
                    val view = layoutInflater.inflate(R.layout.list_item_seriouscrime, parent, false)
                    SeriousCrimeHolder(view)
                }
            }
        }

        override fun getItemCount(): Int = crimes.size

        override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
            val crime = crimes[position]
            when (holder) {
                is NormalCrimeHolder -> holder.bind(crime)
                is SeriousCrimeHolder -> holder.bind(crime)
                else -> throw IllegalArgumentException()
            }
        }

        override fun getItemViewType(position: Int): Int {
            val crime = crimes[position]
            return when (crime.requiresPolice) {
                true -> 1
                else -> 0
            }
        }
    }
}
3 Likes

Great solution! There are a few requirements that you hit on exactly:

  • Adding a property to the Crime entity
  • Checking this property inside of onCreateViewHolder()

Your solution then matches the prompt exactly (two different layouts, two different ViewHolders).

1 Like

I second @jpaone your solution looks great. Maybe the only nitpicky thing (which is more of a personal taste) is that do we really need three view holders i.e. parent view holder, serious view holder and the normal view holder. I’ve just used one view holder which helps me not repeat myself all over the place. Following is my CrimeListFragment.

package com.bignerdranch.android.criminalintent

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

private const val TAG = "CrimeListFragment"

class CrimeListFragment : Fragment() {

    private lateinit var crimeRecyclerView: RecyclerView
    private var adapter: CrimeAdapter? = null

    private val crimeListViewModel: CrimeListViewModel by lazy {
        ViewModelProviders.of(this).get(CrimeListViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "Total crimes: ${crimeListViewModel.crimes.size}")
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime_list, container, false)

        crimeRecyclerView = view.findViewById(R.id.crime_recycler_view) as RecyclerView
        crimeRecyclerView.layoutManager = LinearLayoutManager(context)

        updateUI()

        return view
    }

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

        private lateinit var crime: Crime

        private val titleTextView: TextView = itemView.findViewById(R.id.crime_title)
        private val dateTextView: TextView = itemView.findViewById(R.id.crime_date)

        init {
            itemView.setOnClickListener(this)
        }

        fun bind(crime: Crime) {
            this.crime = crime
            titleTextView.text = this.crime.title
            dateTextView.text = this.crime.date.toString()
        }

        override fun onClick(v: View) {
            Toast.makeText(context, "${crime.title} pressed", Toast.LENGTH_SHORT).show()
        }
    }

    private inner class CrimeAdapter(var crimes: List<Crime>) :
        RecyclerView.Adapter<CrimeHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {
            val view = when (viewType) {
                R.id.list_item_serious_crime -> layoutInflater.inflate(
                    R.layout.list_item_serious_crime, parent, false)
                else -> layoutInflater.inflate(R.layout.list_item_crime, parent, false)
            }
            return CrimeHolder(view)
        }

        override fun getItemCount() = crimes.size

        override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
            val crime = crimes[position]
            holder.bind(crime)
        }

        override fun getItemViewType(position: Int): Int {
            return when {
                crimes[position].requiresPolice -> R.id.list_item_serious_crime
                else -> R.id.list_item_normal_crime
            }
        }
    }

    private fun updateUI() {
        val crimes = crimeListViewModel.crimes
        adapter = CrimeAdapter(crimes)
        crimeRecyclerView.adapter = adapter
    }

    companion object {
        fun newInstance() : CrimeListFragment {
            return CrimeListFragment()
        }
    }
}

one question:
In case it is about showing the Button, why wont we create a button in the same list_view and make it invisible.

and put a condition in the handler, if requiresPolice == 1 make it visible?

handler should pass on all the values and make the button visible where it is needed, unless we can’t put conditions in handler?

@Saher.alsous this is possible I believe you can achieve this in the bind(crime) function in the CrimeHolder however the goal of the exercise was to make use of the Adapter class’ getItemType(int) functionality.

1 Like

Thanks for providing your solution. Works great for me! I did something very close to this but instead of creating an abstract class for the different ViewHolders to inherit from, I did it inheriting directly from RecycleView.ViewHolder(view) but it crashed and didn’t work. Can someone explain to me why do we have to create an abstract class to do this?

(I’ve seen Michael93’s response and while I think is great, I think that by just changing the View and not the ViewHolder you lose some functionality, like setting an onClickListener on the button. Am I correct to think this?)

Thanks if someone responds! (or reads this haha)

I followed along but I couldn’t get more than several itemViews on the screen.

package com.bignerdranch.android.criminalintent

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView


private const val TAG = "CrimeListFragment"
class CrimeListFragment : Fragment() {
    // RecyclerView has 20 errors
    private lateinit var crimeRecyclerView : RecyclerView
    private lateinit var adapter: CrimeAdapter
    private val crimeListViewModel: CrimeListViewModel by lazy {
        ViewModelProvider(this)[CrimeListViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "Total crimes: ${crimeListViewModel.crimes.size}")
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime_list, container, false)
        crimeRecyclerView = view.findViewById(R.id.crime_recycler_view) as RecyclerView
        crimeRecyclerView.layoutManager = LinearLayoutManager(context)
        updateUI()
        return view
        }
    companion object {
        fun newInstance(): CrimeListFragment {
            return CrimeListFragment()
        }
    }
    abstract class CrimeHolder(view: View) : RecyclerView.ViewHolder(view){
        var crime = Crime()
        protected val titleTextView: TextView = itemView.findViewById(R.id.crime_title)
        protected val dateTextView: TextView = itemView.findViewById(R.id.crime_date)
    }
    private inner class NormalHolder(view: View) : CrimeHolder(view), View.OnClickListener {
        init {
            itemView.setOnClickListener(this)
        }

        fun bind(crime: Crime) {
            this.crime = crime
            titleTextView.text = this.crime.title
            dateTextView.text = this.crime.date.toString()
        }

        override fun onClick(v: View) {
            Toast.makeText(context, "${crime.title} pressed!", Toast.LENGTH_SHORT).show()
        }

    }
    private inner class SecondCrimeHolder(view: View) : CrimeHolder(view), View.OnClickListener {
        val contactPoliceButton: Button = itemView.findViewById(R.id.call_police)

        init {
            itemView.setOnClickListener(this)
        }
        fun bind(crime: Crime) {
            this.crime = crime
            titleTextView.text = this.crime.title
            dateTextView.text = this.crime.date.toString()
            contactPoliceButton.setOnClickListener {
                Toast.makeText(context, "This crime is serious!", Toast. LENGTH_SHORT).show()
            }
        }
        override fun onClick(v: View) {
            Toast
                .makeText(context, "${crime.title} pressed!", Toast.LENGTH_SHORT)
                .show()
        }
    }
    private inner class CrimeAdapter(var crimes: List<Crime>) : RecyclerView.Adapter<CrimeHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {
            return when(viewType) {
                0 -> {
                    val view = layoutInflater.inflate(R.layout.list_item_crime, parent, false)
                    NormalHolder(view)
                }
                else -> {
                    val view = layoutInflater.inflate(R.layout.list_item_crime_police, parent, false)
                    SecondCrimeHolder(view)
                }

            }

        }
        override fun getItemCount() : Int = crimes.size

        override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
            val crime = crimes[position]
            when(holder){
                is NormalHolder -> holder.bind(crime)
                is SecondCrimeHolder -> holder.bind(crime)
                else -> throw IllegalArgumentException()
            }
        }

        override fun getItemViewType(position: Int) : Int {
            val crime = crimes[position]
            return when (crime.policeRequired) {
                true -> 1
                else -> 0
            }
        }
    }

    private fun updateUI() {
        val crimes = crimeListViewModel.crimes
        adapter = CrimeAdapter(crimes)
        crimeRecyclerView.adapter = adapter
    }
package com.bignerdranch.android.criminalintent

import androidx.lifecycle.ViewModel

class CrimeListViewModel : ViewModel() {
    val crimes = mutableListOf<Crime>()

    init {
        for (i in 0..100) {
            val crime = Crime()
            crime.title = "Crime #$i"
            crime.isSolved = i % 2 == 0
            crime.policeRequired = when ((0..1).shuffled().first()) {
                0 -> false
                else -> true
            }
            crimes += crime
        }
    }

}
2023-02-20 04:29:30.608   738-1078  vol.VolumeDialogControl com.android.systemui                 E  isCaptionsServiceEnabled failed to check for captions component
                                                                                                    java.lang.IllegalArgumentException: Unknown component: ComponentInfo{com.google.android.as/com.google.android.apps.miphone.aiai.captions.CaptionsService}
                                                                                                    	at android.os.Parcel.createExceptionOrNull(Parcel.java:2430)
                                                                                                    	at android.os.Parcel.createException(Parcel.java:2410)
                                                                                                    	at android.os.Parcel.readException(Parcel.java:2393)
                                                                                                    	at android.os.Parcel.readException(Parcel.java:2335)
                                                                                                    	at android.content.pm.IPackageManager$Stub$Proxy.getComponentEnabledSetting(IPackageManager.java:7204)
                                                                                                    	at android.app.ApplicationPackageManager.getComponentEnabledSetting(ApplicationPackageManager.java:2802)
                                                                                                    	at com.android.systemui.volume.VolumeDialogControllerImpl.onGetCaptionsComponentStateW(VolumeDialogControllerImpl.java:449)
                                                                                                    	at com.android.systemui.volume.VolumeDialogControllerImpl.access$2100(VolumeDialogControllerImpl.java:94)
                                                                                                    	at com.android.systemui.volume.VolumeDialogControllerImpl$W.handleMessage(VolumeDialogControllerImpl.java:826)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:106)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201)
                                                                                                    	at android.os.Looper.loop(Looper.java:288)
                                                                                                    	at android.os.HandlerThread.run(HandlerThread.java:67)
                                                                                                    Caused by: android.os.RemoteException: Remote stack trace:
                                                                                                    	at com.android.server.pm.PackageManagerService.getComponentEnabledSettingInternal(PackageManagerService.java:24382)
                                                                                                    	at com.android.server.pm.PackageManagerService.getComponentEnabledSetting(PackageManagerService.java:24365)
                                                                                                    	at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:3307)
                                                                                                    	at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:8506)
                                                                                                    	at android.os.Binder.execTransactInternal(Binder.java:1179)
---------------------------- PROCESS STARTED (1592) for package com.bignerdranch.android.criminalintent ----------------------------
2023-02-20 04:29:28.847   539-644   StorageManagerService   system_process                       E  Failed to notify volume state changed to the Storage Service
                                                                                                    com.android.server.storage.StorageSessionController$ExternalStorageServiceException: Failed to notify volume state changed for vol : StorageVolume: Virtual SD card
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.notifyVolumeStateChanged(StorageUserConnection.java:420)
                                                                                                    	at com.android.server.storage.StorageUserConnection.notifyVolumeStateChanged(StorageUserConnection.java:131)
                                                                                                    	at com.android.server.storage.StorageSessionController.notifyVolumeStateChanged(StorageSessionController.java:157)
                                                                                                    	at com.android.server.StorageManagerService.onVolumeStateChangedAsync(StorageManagerService.java:1715)
                                                                                                    	at com.android.server.StorageManagerService.access$2600(StorageManagerService.java:204)
                                                                                                    	at com.android.server.StorageManagerService$StorageManagerServiceHandler.handleMessage(StorageManagerService.java:840)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:106)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201)
                                                                                                    	at android.os.Looper.loop(Looper.java:288)
                                                                                                    	at android.os.HandlerThread.run(HandlerThread.java:67)
                                                                                                    Caused by: java.util.concurrent.ExecutionException: android.os.ParcelableException: java.lang.IllegalArgumentException
                                                                                                    	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:361)
                                                                                                    	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1943)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.waitForAsync(StorageUserConnection.java:379)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.waitForAsyncVoid(StorageUserConnection.java:358)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.notifyVolumeStateChanged(StorageUserConnection.java:417)
                                                                                                    	at com.android.server.storage.StorageUserConnection.notifyVolumeStateChanged(StorageUserConnection.java:131) 
                                                                                                    	at com.android.server.storage.StorageSessionController.notifyVolumeStateChanged(StorageSessionController.java:157) 
                                                                                                    	at com.android.server.StorageManagerService.onVolumeStateChangedAsync(StorageManagerService.java:1715) 
                                                                                                    	at com.android.server.StorageManagerService.access$2600(StorageManagerService.java:204) 
                                                                                                    	at com.android.server.StorageManagerService$StorageManagerServiceHandler.handleMessage(StorageManagerService.java:840) 
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:106) 
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201) 
                                                                                                    	at android.os.Looper.loop(Looper.java:288) 
                                                                                                    	at android.os.HandlerThread.run(HandlerThread.java:67) 
                                                                                                    Caused by: android.os.ParcelableException: java.lang.IllegalArgumentException
                                                                                                    	at android.os.ParcelableException$1.createFromParcel(ParcelableException.java:82)
                                                                                                    	at android.os.ParcelableException$1.createFromParcel(ParcelableException.java:79)
                                                                                                    	at android.os.Parcel.readParcelable(Parcel.java:3334)
                                                                                                    	at android.os.Parcel.readValue(Parcel.java:3227)
                                                                                                    	at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
                                                                                                    	at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
                                                                                                    	at android.os.BaseBundle.unparcel(BaseBundle.java:236)
                                                                                                    	at android.os.Bundle.getParcelable(Bundle.java:1002)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.setResult(StorageUserConnection.java:448)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.lambda$waitForAsyncVoid$0$StorageUserConnection$ActiveConnection(StorageUserConnection.java:356)
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection$$ExternalSyntheticLambda0.onResult(Unknown Source:4)
                                                                                                    	at android.os.RemoteCallback.sendResult(RemoteCallback.java:75)
                                                                                                    	at android.os.RemoteCallback$1.sendResult(RemoteCallback.java:52)
                                                                                                    	at android.os.IRemoteCallback$Stub.onTransact(IRemoteCallback.java:89)
                                                                                                    	at android.os.Binder.execTransactInternal(Binder.java:1184)
                                                                                                    	at android.os.Binder.execTransact(Binder.java:1143)
                                                                                                    Caused by: java.lang.IllegalArgumentException
                                                                                                    	at java.lang.reflect.Constructor.newInstance0(Native Method)
                                                                                                    	at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
                                                                                                    	at android.os.ParcelableException.readFromParcel(ParcelableException.java:56)
                                                                                                    	at android.os.ParcelableException$1.createFromParcel(ParcelableException.java:82) 
                                                                                                    	at android.os.ParcelableException$1.createFromParcel(ParcelableException.java:79) 
                                                                                                    	at android.os.Parcel.readParcelable(Parcel.java:3334) 
                                                                                                    	at android.os.Parcel.readValue(Parcel.java:3227) 
                                                                                                    	at android.os.Parcel.readArrayMapInternal(Parcel.java:3624) 
                                                                                                    	at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292) 
                                                                                                    	at android.os.BaseBundle.unparcel(BaseBundle.java:236) 
                                                                                                    	at android.os.Bundle.getParcelable(Bundle.java:1002) 
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.setResult(StorageUserConnection.java:448) 
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection.lambda$waitForAsyncVoid$0$StorageUserConnection$ActiveConnection(StorageUserConnection.java:356) 
                                                                                                    	at com.android.server.storage.StorageUserConnection$ActiveConnection$$ExternalSyntheticLambda0.onResult(Unknown Source:4) 
                                                                                                    	at android.os.RemoteCallback.sendResult(RemoteCallback.java:75) 
                                                                                                    	at android.os.RemoteCallback$1.sendResult(RemoteCallback.java:52) 
                                                                                                    	at android.os.IRemoteCallback$Stub.onTransact(IRemoteCallback.java:89) 
                                                                                                    	at android.os.Binder.execTransactInternal(Binder.java:1184) 
                                                                                                    	at android.os.Binder.execTransact(Binder.java:1143)

The problem was that the second holder list_item_crime_police.xml had its height set to match_parent. If only I had scrolled down, I would have seen them all on there. Novice mistake I guess. At least now I can move on.