Anko: Creating ViewHolder view using anko-component

Created on 17 May 2016  路  7Comments  路  Source: Kotlin/anko

How can I create the view using the AnkoComponent for a ViewHolder.

I am trying to remove the dependency of the UI from the ViewHolder using a component.

fixed question

Most helpful comment

I am writing blog post about it. It will be released soon but in the meantime have a look at code samples:

class LunchMenuAdapter(var mealList: List<Meal>) : RecyclerView.Adapter<LaunchMenuItemViewHolder>() {
   override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): LaunchMenuItemViewHolder? {
       return LaunchMenuItemViewHolder(LunchMenuItemUI().createView(AnkoContext.create(parent!!.context, parent)))
   }

   override fun onBindViewHolder(holder: LaunchMenuItemViewHolder?, position: Int) {
       val meal = mealList[position]
       holder!!.bind(meal)
   }

   override fun getItemCount(): Int {
       return mealList.size
   }
}

class LunchMenuItemUI : AnkoComponent<ViewGroup> {
   override fun createView(ui: AnkoContext<ViewGroup>): View {
       return with(ui) {
           linearLayout {
               lparams(width = matchParent, height = dip(48))
               orientation = LinearLayout.HORIZONTAL
               textView {
                   id = R.id.lunch_menu_name
                   textSize = 16f
               }
           }
       }
   }
}


class LaunchMenuItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
   val name: TextView = itemView.find(R.id.lunch_menu_name)

   fun bind(meal: Meal) {
       name.text = meal.name
   }
}

All 7 comments

I am writing blog post about it. It will be released soon but in the meantime have a look at code samples:

class LunchMenuAdapter(var mealList: List<Meal>) : RecyclerView.Adapter<LaunchMenuItemViewHolder>() {
   override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): LaunchMenuItemViewHolder? {
       return LaunchMenuItemViewHolder(LunchMenuItemUI().createView(AnkoContext.create(parent!!.context, parent)))
   }

   override fun onBindViewHolder(holder: LaunchMenuItemViewHolder?, position: Int) {
       val meal = mealList[position]
       holder!!.bind(meal)
   }

   override fun getItemCount(): Int {
       return mealList.size
   }
}

class LunchMenuItemUI : AnkoComponent<ViewGroup> {
   override fun createView(ui: AnkoContext<ViewGroup>): View {
       return with(ui) {
           linearLayout {
               lparams(width = matchParent, height = dip(48))
               orientation = LinearLayout.HORIZONTAL
               textView {
                   id = R.id.lunch_menu_name
                   textSize = 16f
               }
           }
       }
   }
}


class LaunchMenuItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
   val name: TextView = itemView.find(R.id.lunch_menu_name)

   fun bind(meal: Meal) {
       name.text = meal.name
   }
}

I have an alternative approach. I use to use the same method, but got frustrated maintaining the ids.xml file and the needless overhead of the findViewById. I now use the following:

class LunchMenuAdapter(var mealList: List<Meal>
): RecyclerView.Adapter<LunchMenuItemViewHolder>() {
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LunchMenuItemViewHolder {
       return LunchMenuItemUI().createView(
                AnkoContext.create(parent.context, parent)).tag as LunchMenuItemViewHolder
   }

   override fun onBindViewHolder(holder: LaunchMenuItemViewHolder, position: Int) {
       val meal = mealList[position]
       holder.bind(meal)
   }

   override fun getItemCount(): Int = mealList.size
}
class LunchMenuItemUI: AnkoComponent<ViewGroup> {
    override fun createView(ui: AnkoContext<ViewGroup>): View {
        var name: TextView by Delegates.notNull()
        var itemView = with(ui) {
            linearLayout {
                lparams(width = matchParent, height = dip(48))
                orientation = LinearLayout.HORIZONTAL
                textview {
                    name = this // Save a reference as you create the object
                    textSize = 16f
                }
            }
        }
        itemView.tag = LunchMenuItemViewHolder(itemView, name)
        return itemView
    }
}
class LunchMenuItemViewHolder(
    itemView: View,
    val name: TextView
): RecyclerView.ViewHolder(itemView) {
    fun bind(meal: Meal) {
        name.text = mean.name
    }
}

Note that the ViewHolder is created along with the view and is returned as the view's tag.

As a note for the great folk @anko, MVP (Model, View, Presenter) is really the best way to build UI and AnkoComponents get so close but not quite there in enabling this easily. Trying to adapt the lessons at Google's codelab is difficult with AnkoComponents. In java we'd have (using the example above) a new View class that implemented some LunchMenuContract.View interface but in Anko we only create an instance of a View, not a new type. I've played around with a few ideas but nothing seemed right (I tried creating an extension property for the view but I can't get it to specify a type, I tried some combination of typealias to similarly return a custom view type but I couldn't get it working). I wish I had more ideas on this, but the desired end result would be to return a view that also extends the LunchMenuContract.View interface.

Update:

I've found a better way, I'm now using the following in my own Custom.kt file:

interface AnkoViewProvider {
    val view: View
}

inline fun <T> ViewManager.ankoComponent(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): View
        where T: AnkoComponent<Context>, T: AnkoViewProvider {
    val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)
    val ui = factory(ctx)
    ui.init()
    AnkoInternals.addView(this, ui.view)
    return ui.view
}

This can be used to create the following:

inline fun ViewManager.lunchMenuItem(theme: Int = 0) = lunchMenuItem(theme) {}
inline fun ViewManager.lunchMenuItem(theme: Int = 0, init: LunchMenuItemUI.() -> Unit): View =
    ankoComponent({LunchMenuItemUI(it)}, theme, init)

class LunchMenuItemUI(private val context: Context): AnkoComponent<ViewGroup>, AnkoViewProvider {
    override val view: View by lazy {
        createView(AnkoContext.create(context))
    }

    override fun createView(ui: AnkoContext<ViewGroup>): View {
        var name: TextView by Delegates.notNull()
        var itemView = with(ui) {
            linearLayout {
                lparams(width = matchParent, height = dip(48))
                orientation = LinearLayout.HORIZONTAL
                textview {
                    name = this // Save a reference as you create the object
                    textSize = 16f
                }
            }
        }
        itemView.tag = LunchMenuItemViewHolder(itemView, name)
        return itemView
    }
}

While this doesn't quite solve the issue with ViewHolder's requirement to create the View prior to the constructor call, it is a nice way to create custom views with functionality in an AnkoComponent. The ViewHolder can be added in the same manner as the view property. The final step would be to be able to also have the component extend ViewHolder.

Your answer is very helpful to me. And I'm wondering if we could have an AnkoViewHolder interface to summary all the functionality a ViewHolder could have. Code as below:

abstract class AnkoViewHolder<in T>(ctx: Context) : AnkoViewProvider, AnkoComponent<Context> {

    abstract override fun createView(ui: AnkoContext<Context>): View

    abstract fun bindData(data: T)

    override val view: View by lazy {
        createView(AnkoContext.Companion.create(ctx))
    }
}

Then if we want to crate a ViewHolder-Pattern Class, we could write like this:

class ForumListViewHolder(ctx: Context) : AnkoViewHolder<ForumListData>(ctx) {

    private var name: TextView by Delegates.notNull()
    private var image: SimpleDraweeView by Delegates.notNull()
    private var threads: TextView by Delegates.notNull()

    override fun bindData(data: ForumListData) {
        name.text = data.name
        image.imageUrl = data.icon
        threads.text = data.threads
    }

    override fun createView(ui: AnkoContext<Context>): View = with(ui) {
        return linearLayout {
            orientation = LinearLayout.HORIZONTAL

            image = simpleDraweeView {
            }.lparams(width = dip(80 * 1.65f),
                    height = dip(80)) {
                margin = dip(10)
            }
            verticalLayout {
                name = textView {
                    textSize = 16f
                    textColor = Color.BLACK
                }.lparams {
                    width = matchParent
                    height = wrapContent
                }
                threads = textView {
                    textSize = 13f
                    textColor = resources.getColor(R.color.customRed)
                }.lparams {
                    width = matchParent
                    height = wrapContent
                }

            }.lparams(width = 0, height = wrapContent) {
                weight = 1f
                margin = dip(10)
            }
        }
    }

}

And in Adapter Class(note I'm using the ListView):

 override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        return if (convertView == null) {
            val viewHolder = ForumListViewHolder(ctx)
            val view = viewHolder.view
            view.tag = viewHolder
            viewHolder.bindData(data[position])
            view
        } else {
            (convertView.tag as ForumListViewHolder).bindData(data[position])
            convertView
        }
    }

Is there any way better than passing "R.id.xxx" to viewHolder or tag as viewholder in anko way?
Found it by myself, just passingby the ui class to viewholder:

```kotlin
class MessageAdapter(val mainActivity: MainActivity) : RecyclerView.Adapter(), AnkoLogger {

private var msgSet: MutableList<MessageData> = mutableListOf()

fun insertMessage(msgs: List<MessageData>) {
    msgSet.addAll(msgs)
    notifyDataSetChanged()
}

class ViewHolder(itemView: View,val messageItemUI: MessageItemUI) : RecyclerView.ViewHolder(itemView), AnkoLogger {
    fun bind(msg: MessageData) {
        val avatar: CircleImageView = messageItemUI.avatar
        val name: TextView = messageItemUI.name
        val date: TextView = messageItemUI.date

        name.text = msg.msg
        val time = Date(msg.time)
        val formatter = SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
        date.text = formatter.format(time)
        val url = msg.kid_compare_image_url
        if (url != null) {
            val tool = Tools()
            tool.urlImg(url) { bmp ->
                if (bmp != null) avatar.setImageBitmap(bmp)
            }
        }

    }
}


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val messageItemUI = MessageItemUI()
    val viewHolder = ViewHolder(messageItemUI.createView(
            AnkoContext.create(parent.context, parent)),messageItemUI)
    return viewHolder
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val msg = msgSet[position]
    holder.bind(msg)
    holder.itemView.onClick {
        mainActivity.startActivity<MessageDetailActivity>("id" to msg.msg_id)
    }
}

override fun getItemCount(): Int = msgSet.size

}
```

I have created companion ids to the anko component so that they can be used to identify in view holders. Let me know your thoughts about that:

class MovieUI : AnkoComponent<ViewGroup>{

    companion object {
        val tvTitleId = 1
        val tvYearId = 2
    }

    override fun createView(ui: AnkoContext<ViewGroup>): View = with(ui){
        verticalLayout {
            lparams(matchParent, wrapContent)
            padding = dip(16)

            textView {
                id = tvTitleId
                layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
                text = "Sherlock"
                textSize = 16f // <- it is sp, no worries
                textColor = Color.BLACK
            }

            textView {
                id = tvYearId
                layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
                text = "2009"
                textSize = 14f
            }
        }
    }
}

And from the view holder, you may access them in this way:

    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        var tvTitle: TextView
        var tvYear: TextView

        init {
            tvTitle = itemView.findViewById(MovieUI.tvTitleId)
            tvYear = itemView.findViewById(MovieUI.tvYearId)
        }

    }

This approach is handy since ids are tied to the UI component itself, so wherever you re-use the component, it is easy to find the ids rather than searching through ids.xml.

I've just been tackling with this issue as well. My main goal is to leverage the performance efficiencies of no inflation and no findViewbyId. The no inflation issue is already solved here, but I was struggling to avoid findViewById. Below is my solution. Would love to hear thoughts.

class LunchMenuAdapter(var mealList: List<Meal>) : RecyclerView.Adapter<LaunchMenuItemViewHolder>() {
   override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): LaunchMenuItemViewHolder? {
       return LaunchMenuItemViewHolder(LunchMenuItemUI(AnkoContext.createReusable(parent!!.context, parent)))
   }

   override fun onBindViewHolder(holder: LaunchMenuItemViewHolder?, position: Int) {
       val meal = mealList[position]
       holder!!.bind(meal)
   }

   override fun getItemCount(): Int {
       return mealList.size
   }
}

class LunchMenuItemUI(ankoContext: AnkoContext<ViewGroup>) : AnkoComponent<ViewGroup> {
   val container: LinearLayout
   lateinit var name: TextView

   init {
      container = createView(ankoContext)
   }

   override fun createView(ui: AnkoContext<ViewGroup>): View {
       return with(ui) {
           linearLayout {
               lparams(width = matchParent, height = dip(48))
               orientation = LinearLayout.HORIZONTAL
               name = textView {
                   id = R.id.lunch_menu_name
                   textSize = 16f
               }
           }
       }
   }
}


// The ViewHolder takes in a LunchMenuItemUI rather than a View
class LaunchMenuItemViewHolder(itemView: LunchMenuItemUI) : RecyclerView.ViewHolder(itemView.container) {
   val name: TextView = itemView.name // No findViewById needed here

   fun bind(meal: Meal) {
       name.text = meal.name
   }
}
Was this page helpful?
0 / 5 - 0 ratings