Refactor my viewholder class in kotlin - android

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])
}

Related

lateinit property binding has not been initialized

I know that there are similar questions to this, but I just cant find something that is similar, I've been studying new things to learn, and while converting kotlin synthetics to viewvbinding mode, I've encountered this error
kotlin.UninitializedPropertyAccessException: lateinit property binding has not been initialized
at com.codepalace.chatbot.ui.MessagingAdapter.onBindViewHolder(MessagingAdapter.kt:60)
at com.codepalace.chatbot.ui.MessagingAdapter.onBindViewHolder(MessagingAdapter.kt:17)
It says that I have to initialize the binding, but I dont know where to put it.
This is the code.
class MessagingAdapter: RecyclerView.Adapter<MessagingAdapter.MessageViewHolder>() {
var messagesList = mutableListOf<Message>()
private lateinit var binding: MessageItemBinding
inner class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return MessageViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.message_item, parent, false)
)
}
override fun getItemCount(): Int {
return messagesList.size
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val currentMessage = messagesList[position]
when (currentMessage.id) {
SEND_ID -> {
holder.itemView.findViewById<View>(R.id.tv_message).apply {
binding.tvMessage.text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.findViewById<View>(R.id.tv_bot_message).visibility = View.GONE
}
RECEIVE_ID -> {
holder.itemView.findViewById<View>(R.id.tv_bot_message).apply {
binding.tvBotMessage.text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.findViewById<View>(R.id.tv_message).visibility = View.GONE
}
}
}
fun insertMessage(message: Message) {
this.messagesList.add(message)
notifyItemInserted(messagesList.size)
}
private lateinit var binding: MessageItemBinding
You didn't initialize the binding object, as you defined it as lateinit, then you should define it.
This is typically in onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackedActivityHolder {
val inflater = LayoutInflater.from(parent.context)
binding = DataBindingUtil.inflate<MessageItemBinding>(inflater, R.layout.message_item, parent, false)
return MessageViewHolder(
binding
)
}
UPDATE
You need to accept MessageItemBinding type instead of View in the MessageViewHolder constructor, and use itemView.root to get the root view of the list item.
inner class MessageViewHolder(itemView: MessageItemBinding) : RecyclerView.ViewHolder(itemView.root) {
init {
itemView.root.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
The reason why You get this error is that MessageItemBinding should not be in Adapter but in ViewHolder class. You can make RecyclerView like this:
object MessageDiffCallback : DiffUtil.ItemCallback<Message>()
{
override fun areItemsTheSame(
oldItem: Message,
newItem: Message
): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: Message,
newItem: Message
): Boolean =
oldItem == newItem
}
I assume that Message is a data class. You have to create this MessageDiffCallback and override these 2 methods in a similar way that I did it.
Now Create ViewHolder class:
class MessageViewHolder private constructor(
private val binding: MessageItemBinding
) : RecyclerView.ViewHolder(binding.root)
{
companion object
{
fun create(parent: ViewGroup): MessageViewHolder
{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = MessageItemBinding.inflate(layoutInflater, parent, false)
return MessageViewHolder(
binding
)
}
}
fun bind(
message: Messaage
)
{
// Here You can do everything.
// You pass the message and based on this You can set up a view and use binding
}
}
And now Adapter class
class MessageAdapter : ListAdapter<Message, MessageViewHolder>(MessageDiffCallback)
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder =
MessageViewHolder.create(parent)
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) =
holder.bind(
message = getItem(position),
)
}
Now You should have a working adapter. To put data in it use messageAdapter.submitList(messages) in Your Fragment/Activity
Maybe not the best answer because it changes Your code and uses a little different logic but it should work better. You can check google sample code here or take codelab here. It is free after signing up

android - Creating and using ViewBinder inside a RecyclerView adapter

In my app, there is an Activity which has a RecyclerView inside, which loads the list of options needed for that screen.
In the code below, i tried to implement a binder, which is needed because of the recent Android changes.
However, when i open the activity starts, the application crashes, throwing this error, linking the line with binding = ItemSettingsBinding.bind(binding.root):
kotlin.UninitializedPropertyAccessException: lateinit property binding has not been initialized
What am i doing wrong? What's the correct way to implement a binder inside an adapter?
AdapterSettings.kt
class AdapterSettings(
var settingsList: List<DataItemSettings>,
var listener: OnItemClickListener
) : RecyclerView.Adapter<AdapterSettings.SettingsViewHolder>() {
private lateinit var binding: ItemSettingsBinding
inner class SettingsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(p0: View?) {
val position : Int = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.OnItemClick(position)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_settings, parent, false)
return SettingsViewHolder(view)
}
override fun getItemCount(): Int {
return settingsList.size
}
override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) {
binding = ItemSettingsBinding.bind(binding.root)
holder.itemView.apply {
binding.rvTitle.text = settingsList[position].stringTitle
binding.rvDescription.text = settingsList[position].stringDescription
binding.rvIcon.setImageResource(settingsList[position].itemIcon)
}
}
interface OnItemClickListener {
fun OnItemClick(position: Int)
}
}
I believe you're missing your inflate in onCreateViewHolder:
// Pseudo-Code
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder {
val binding = ItemSettingsBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return SettingsViewHolder(binding)
}
Then you can make use of it.
Create the binding in onCreateViewHolder and pass the binding into the ViewHolder instead of the inflated View. Thus you create a binding for each created view and only need to do the apply stuff in the onBindViewHolder
Example:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_settings, parent, false)
val binding = ItemSettingsBinding.bind(view)
return SettingsViewHolder(binding)
}
override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) {
holder.binding.apply {
rvTitle.text = settingsList[position].stringTitle
rvDescription.text = settingsList[position].stringDescription
rvIcon.setImageResource(settingsList[position].itemIcon)
}
}
Adapt your ViewHolder accordingly
There is indeed another way to ViewBind in an adapter.
First, we need to setup the ViewHolder in a different way:
inner class SettingsViewHolder(private val binding: ItemSettingsBinding):
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
With this, we created a binding value inside the brackets, so we are able to call the items of the actual view or layout trough binding.root
Inside the viewholder, we need to create a function used to bind our items. We can either bind like this:
fun bind(item: Item) {
binding.item = item
binding.executePendingBindings()
}
Or like this:
fun bind(item: DataItemSettings) {
binding.rvTitle.text = settingsList[position].stringTitle
binding.rvDescription.text = settingsList[position].stringDescription
binding.rvIcon.setImageResource(settingsList[position].itemIcon)
}
NOTICE: 'getter for position: Int' is deprecated. Deprecated in Java.
And, final step, we need to write this, inside bindViewHolder:
override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) {
holder.bind(settingsList[position])
}

Implementation of several views in one recyclerViews

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
}
}

Editing list item on list view

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.

RecyclerView data binding with custom item configuration

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.

Categories

Resources