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
}
}
}
}
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()
}
}
}
@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.
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?)
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.