How can I implement different views in one recyclerViews in Kotlin ???
I want to create an application containing legal codes. My problem is that individual legal provisions are divided into chapters. And if I can create a progran that will display all the recipes for me, I don't really know how to put it in the recyclerView between the layout with specific legal provisions layout with information about the number and title of the chapter.
The code below still shows me the same view.
package pl.nynacode.naukapraw
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.cart_view_legal_name.view.*
import kotlinx.android.synthetic.main.chapter_layout.view.*
class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>(){
class MyViewHolder(val view: View, val view2: View):RecyclerView.ViewHolder(view) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater= LayoutInflater.from(parent.context);
val legalName = layoutInflater.inflate(R.layout.cart_view_legal_name, parent ,false);
val chapterName = layoutInflater.inflate(R.layout.chapter_layout, parent,false);
return MyViewHolder(legalName, chapterName);
}
override fun getItemCount(): Int {
return KodeksKarny.nrArticle.size;
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
when(position){
0->{
val chapter = holder.view2.tvChapterName;
chapter.setText(KodeksKarny.nrArticle[position])
}
else->{
val nrArticle = holder.view.nrArt;
val textArticle=holder.view.txtArt;
nrArticle.setText(KodeksKarny.nrArticle[position]);
textArticle.setText(KodeksKarny.txtArticle[position]);
// obsługa klikniecia na przycisk
nrArticle.setOnClickListener{
if (textArticle.visibility == View.GONE){
textArticle.visibility = View.VISIBLE
}else textArticle.visibility = View.GONE
}
}
}
}
}
I will add that I'm a beginner and I can't do much yet
RecyclerView.Adapter has a method called fun getItemViewType(position: Int): Int that returns the type of view on a given position.
Based on that function you can create different view holders or pass to the same view holder type different layouts (but avoid last one).
You simply need to override a function in your adapter and decide the type of an item at that position:
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
// the code below is just an example.
val type = when (item) {
is Header -> HEADER_TYPE
is NotHeader -> NOT_HEADER_TYPE
}
return type
}
Where you could define these types? In companion object for example:
class YourAdapter: ... {
companion object {
private const val HEADER_TYPE = 0
private const val NOT_HEADER_TYPE = 1
}
...
}
Later in onCreateViewHolder and onBindViewHolder you can create different view holders and bind to those view holders the data you have.
class YourAdapter: ... {
companion object {
private const val HEADER_TYPE = 0
private const val NOT_HEADER_TYPE = 1
}
...
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
// the code below is just an example.
val type = when (item) {
is Header -> HEADER_TYPE
is NotHeader -> NOT_HEADER_TYPE
}
return type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
if (viewType == HEADER_TYPE) {
// Here you create HeaderViewHolder
} else {
val layoutInflater= LayoutInflater.from(parent.context);
val legalName = layoutInflater.inflate(R.layout.cart_view_legal_name, parent ,false);
val chapterName = layoutInflater.inflate(R.layout.chapter_layout, parent,false);
return MyViewHolder(legalName, chapterName);
}
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val itemViewType = getItemViewType(position)
if (itemViewType == HEADER_TYPE) {
// cast MyViewHolder to HeaderViewHolder, for example
val header = viewHolder as HeaderViewHolder
header.headerTitle.text = ...
} else {
val nrArticle = holder.view.nrArt;
... other type
}
}
}
Here are the official tutorials on how to create an adapter with different types of views.
What I personally prefer is to implement abstract class BaseViewHolder: RecyclerView.ViewHolder that will be used as a generic type argument of your adapter implementation. This BaseViewHolder should have an abstract method, like abstract fun bind(data: YourDataType). The function will be implemented by view holders that will extend the BaseViewHolder class.
Also, as Kotlin provides us with sealed classes I prefer to create a sealed class and objects that extend from it to hold view holder types so when you implement your onCreateViewHolder method it could avoid else case. But that is just what I like and is not required in any way.
An example of sealed class + objects + onCreateViewHolder:
sealed class Types(val rawType: Int) {
object Header: Types(0)
object NotHeader: Types(1)
companion object {
fun from(rawType: Int) =
when (rawType) {
Header.rawType -> Header
NotHeader.rawType -> NotHeader
else -> throw RuntimeException("No such type")
}
}
}
class YourAdapter ... {
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
// the code below is just an example.
val type = when (item) {
is Header -> Types.Header.rawType
is NotHeader -> Types.NotHeader.rawType
}
return type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder =
when (Types.from(viewType)) {
is Types.Header -> // return HeaderViewHolder
is Types.NotHeader -> // return NotHeaderViewHolder
}
}
Related
I am using recyclerView to show data from firebase database and I want to handle clicks,
Now the important part is that I want to know the number that was clicked in order to test google play in app billing before showing the next activity
I mean user should click item number one then pay to see information number 1 and so on
Any help, please ?
//my adapter
class MyAdapter(
private val arrayList: ArrayList<Long>
) :
RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_item, parent, false)
return MyViewHolder(view)
}
override fun getItemCount() = arrayList.size
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.number.text = arrayList[position].toString()
}
class MyViewHolder(view: View) :
RecyclerView.ViewHolder(view) {
val number = view.findViewById<View>(R.id.singleNumberId) as TextView
}
}
Here is a small example I have of registering a click for a RecyclerView adapter item:
class PatientListAdapter : ListAdapter<Patient, PatientListAdapter.PatientViewHolder>(co.za.abcdefgh.viewmodels.PatientListViewModel.DiffItemCallback) {
// this property will be used to set the onclick callback for the entire adpater
var onPatientSelectedCallback: PatientSelectedCallback? = null
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PatientViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_patient, parent, false) as View
return PatientViewHolder(view)
}
override fun onBindViewHolder(holder: PatientViewHolder, position: Int) {
holder.view.item_patient_name.text = getItem(position).toString()
holder.view.item_patient_folderNumber.text = getItem(position).folderNumber
// lets set our on click for each viewholder here
holder.view.item_patient_info_card.setOnClickListener {
// the secret sauce .... getItem(holder.adapterPosition)
onPatientSelectedCallback?.onPatientSelected(getItem(holder.adapterPosition))
}
}
class PatientViewHolder(val view: View) : RecyclerView.ViewHolder(view)
// interface which defines a method signature that will called when a item in the adpater is selected
interface PatientSelectedCallback {
fun onPatientSelected(patient: Patient)
}
}
and then wherever you use the adapter after instantiating simply do:
val viewAdapter = PatientListAdapter()
viewAdapter.onPatientSelectedCallback =
object : PatientListAdapter.PatientSelectedCallback {
override fun onPatientSelected(patient: Patient) {
// do something with the chosen item
patientViewModel.setPatient(patient)
}
}
Imagine we have a simple list of items. Each item contains only a short title.
To handle the list we are using RecyclerView with ListAdapter and ViewHolders.
Each item/view is not editable unless we click it.
In this scenario I am using one view model for list and one for item under edit.
Unfortunately all my attempts failed.
I have tried to use two different view holders but the list was flickering, after all inflating view (in this case binding) is heavy.
Another shot I was giving to use the same view holder but with two various bind methods - one binding plain item, second binding with viewmodel instead of data object but it failed as well - suddenly a few rows were editable.
Has anyone solved it ?
class MistakesAdapter(private val editViewModel: MistakeEditViewModel) :
ListAdapter<Mistake, RecyclerView.ViewHolder>(MistakesDiffCallback()) {
companion object{
const val ITEM_PLAIN_VIEW_TYPE = 0
const val ITEM_EDITABLE_VIEW_TYPE = 1
}
private var itemPositionUnderEdit = -1
private val listener = object: MistakeItemListener{
override fun onClick(view: View, position: Int) {
Timber.d("OnClick : edit - $itemPositionUnderEdit, clickPos - $position")
editViewModel.onEditMistake(getItem(position))
itemPositionUnderEdit = position
notifyItemChanged(itemPositionUnderEdit)
}
}
override fun getItemViewType(position: Int) =
when (position) {
itemPositionUnderEdit -> ITEM_EDITABLE_VIEW_TYPE
else -> ITEM_PLAIN_VIEW_TYPE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (viewType) {
ITEM_EDITABLE_VIEW_TYPE -> EditableMistakeViewHolder.from(parent)
else -> MistakeViewHolder.from(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EditableMistakeViewHolder -> holder.bind(editViewModel, listener)
is MistakeViewHolder -> holder.bind(getItem(position), listener)
else -> throw ClassCastException("Unknown view holder type")
}
}
class MistakeViewHolder private constructor(private val binding: ListItemMistakesBinding) :
RecyclerView.ViewHolder(binding.root) {
companion object {
fun from(viewGroup: ViewGroup): MistakeViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val binding = ListItemMistakesBinding.inflate(inflater, viewGroup, false)
return MistakeViewHolder(binding)
}
}
fun bind(item: Mistake, listener: MistakeItemListener) {
binding.apply {
mistake = item
inputType = InputType.TYPE_NULL
this.listener = listener
position = adapterPosition
executePendingBindings()
}
}
}
class EditableMistakeViewHolder private constructor(private val binding: ListItemMistakesBinding)
: RecyclerView.ViewHolder(binding.root) {
companion object{
fun from(viewGroup: ViewGroup): EditableMistakeViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val binding = ListItemMistakesBinding.inflate(inflater, viewGroup, false)
return EditableMistakeViewHolder(binding)
}
}
fun bind(viewModel: MistakeEditViewModel, listener: MistakeItemListener){
binding.apply {
this.viewModel = viewModel
inputType = InputType.TYPE_CLASS_TEXT
this.listener = listener
position = adapterPosition
root.setBackgroundColor(Color.GRAY)
}
}
}
}
class MistakeEditViewModel(private val repository: MistakesRepository) : ViewModel() {
#VisibleForTesting
var mistakeUnderEdit: Mistake? = null
//two-way binding
val mistakeName = MutableLiveData<String>()
fun onEditMistake(mistake: Mistake) {
mistakeUnderEdit = mistake
mistakeName.value = mistake.name
}
}
By changing my approach to the problem I solved it.
I make all list items editable but at the same time I am following focus.
To cut the long story short, I invoke item view model methods with help of OnFocusChangeListener and TextWatcher on my editTexts.
I'm new to Android development (and Kotlin).
I'm trying to implement a RecyclerView (which works fine) and when I click on a specific row it opens a new activity (Intent).
However, whenever I've press/click on one of the rows, I'm only able to get the value "-1" returned.
I've tried a number of different approaches (you should see the number of tabs in my browser).
This seems like it should be a fairly straightforward occurrence for something as common as a RecyclerView, but for whatever reason I'm unable to get it working.
Here is my RecyclerView Adapter file:
class PNHLePlayerAdapter (val players : ArrayList<PNHLePlayer>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var onItemClick: ((Int)->Unit) = {}
// Gets the number of items in the list
override fun getItemCount(): Int {
return players.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(context).inflate(
R.layout.pnhle_list_item,
parent,
false
)
val viewHolder = ViewHolder(itemView)
itemView.setOnClickListener {
onItemClick(viewHolder.adapterPosition)
}
return ViewHolder(itemView)
}
// Binds each item in the ArrayList to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvPlayerName?.text = players[position].Name
holder.tvPlayerRank?.text = position.toString()
holder.tvPNHLe?.text = players[position].PNHLe.toString()
holder.tvTeam?.text = players[position].Team
holder.ivLeague?.setImageResource(leagueImageID)
}
}
class ViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val linLayout = view.hor1LinearLayout
val ivTeam = view.teamImageView
val tvPlayerName = view.playerNameTextView
val tvPlayerRank = view.rankNumTextView
val tvPNHLe = view.pnhleTextView
val tvTeam = view.teamTextView
val ivLeague = view.leagueImageView
}
As you can see, there is a class property "onItemClick" which uses a lambda as the click callback.
I setOnClickListener in the onCreateViewHolder method after the view is inflated.
Next, in my Activity I add the list to my Adapter and set the call back.
However, every time I 'Toast' the position it is displayed as '-1'.
val adapter = PNHLePlayerAdapter(list, this)
adapter.onItemClick = { position ->
Toast.makeText(this, position.toString(),Toast.LENGTH_SHORT).show()
var intent = Intent(this, PlayerCardActivity::class.java)
//startActivity(intent)
}
rv_player_list.adapter = adapter
Perhaps I'm not thinking about this properly, but shouldn't the position represent the row number of the item out of the RecyclerView???
Ideally, I need to use the position so that I can obtain the correct item from the 'list' (ArrayList) so that I can pass information to my next Activity using the Intent
I found the issue.
Change this line in onCreateViewHolder:
return ViewHolder(itemView)
to this one:
return viewHolder
I would reorganize the adapter like this:
class PNHLePlayerAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<Adapter.ViewHolder>() {
interface AdapterListener {
fun onItemSelected(position: Int?)
}
var players: List<Player> = listOf()
set(value) {
field = value
this.notifyDataSetChanged()
}
var listener: AdapterListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_car_selector, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return brands.size
}
inner class ViewHolder(view: View): androidx.recyclerview.widget.RecyclerView.ViewHolder(view) {
private var position: Int? = null
private val baseView: LinearLayout? = view.findViewById(R.id.baseView) as LinearLayout?
...
init {
baseView?.setOnClickListener {
listener?.onManufacturerSelected(position)
}
}
fun bind(position: Int) {
this.position = position
...
}
}
}
And from your activity/fragment set the listener as adapter.listener = this, and implement the onItemSelected(position: Int?)
override fun onItemSelected(position: Int?) {
...
}
My recycler view has two types of item view. One type of them has MPAndroidChart in it. I need to do some chart view configuration that cannot be done in XML. How can I do it given that I am using RecyclerView data binding with a single base view holder (as recommended by George Mount) ?
open class BaseViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(obj: Any) {
binding.setVariable(BR.obj, obj)
binding.executePendingBindings()
}
}
abstract class BaseAdapter : RecyclerView.Adapter<BaseViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)
return BaseViewHolder(binding)
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
val obj = getObjForPosition(position)
holder.bind(obj)
}
override fun getItemViewType(position: Int): Int {
return getLayoutIdForPosition(position)
}
protected abstract fun getObjForPosition(position: Int): Any
protected abstract fun getLayoutIdForPosition(position: Int): Int
}
You can still access
holder.itemView.myChartViewId.doSomeStuff()
on the onBindViewHolder() call.
You can also implement a function to "initialize" your charts in your view holder like this:
open class BaseViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(obj: Any) {
binding.setVariable(BR.obj, obj)
binding.executePendingBindings()
}
fun initCharts() {
if (itemView.myChartViewId == null) return
itemView.myChartViewId.doSomwStuff()
}
}
and call it whenever you need.
I have a recycler list which holds many different types of item views. It is quite easy to use databinding without necessary to declare the layout and assignment in the viewholder, however I end up with many biloplate code to just create the different viewholders with databinding, is there a way to get rid of them?
class ViewHolder1 private constructor(
val binding: ViewHolder1LayoutBinding
): RecyclerView.ViewHolder(binding.root) {
companion object {
fun create(parent: ViewGroup): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ViewHolder1LayoutBinding.inflate(inflater, parent, false)
return ViewHolder1(binding)
}
}
fun bind(viewModel: ViewHolder1ViewModel) {
binding.viewModel = viewModel
binding.executePendingBindings()
}
}
kotlin supports view binding so no need to do other stuffs for binding view.
Just follow steps and you will able to access view by its id defined in xml layout.
In app level gradle add following
apply plugin: 'kotlin-android-extensions'
Import view
import kotlinx.android.synthetic.main.<layout_file>.view.*
Just check this class for demo
class NotificationHolder(itemView: View?, listener: NotificationItemListener) : RecyclerView.ViewHolder(itemView) {
init {
itemView?.setOnClickListener {
listener.onNotificationItemClicked(adapterPosition)
}
}
fun bind(notificationModel: NotificationModel) {
val titleArray = notificationModel.title.split("#".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
itemView.tvNotificationTitle.text = titleArray[0]
itemView.tvNotificationDetails.text = notificationModel.message
itemView.tvNotificationTime.text = notificationModel.formattedTime
Glide.with(itemView.context).load(ServiceHandler.BASE_URL + notificationModel.icon).dontAnimate().diskCacheStrategy(DiskCacheStrategy.SOURCE).error(R.drawable.user_default_logo).into(itemView.imageView)
if (CommonUtils.lastNotificationTime < notificationModel.date) {
itemView.card.setCardBackgroundColor(Color.parseColor("#ffffff"))
} else {
itemView.card.setCardBackgroundColor(Color.parseColor("#f2f2f2"))
}
}
}
In adapter you can override
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0 || viewType == 3) {
NotificationHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_notification, parent, false), this)
} else {
NotificationListHeaderHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_notification_header, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
(holder as? NotificationHolder)?.bind(notificationList[position])
(holder as? NotificationListHeaderHolder)?.bind(notificationList[position])
}