I'm looking for a way to find the context of a RecyclerView Adapter outside of the viewholder methods. I have two different arrays that I would like to be able to display based on the activity/fragment that is being shown(the final app would have 4 arrays which is why I don't just want to make another adapter for each array). I can get it to work in the onbindviewholder using holder.itemview.context but since the arrays aren't the same size I need a way to use context for an if statement in getitemcount() as well. Passing context in the constructor results in "no value passed for parameter". Was wondering if anyone has found a solution to this problem using either fragments or activities. Worst case scenario I just make a different adapter for every array
class ItemAdapter(var context: Context) : RecyclerView.Adapter<ItemAdapter.ViewHolder>() {
private var weeklyTasks = arrayOf(R.string.app_name, R.string.daily_hint_1, R.string.daily_hint_2, R.string.daily_hint_3)
private var dailyTasks = arrayOf(R.string.daily_hint_1, R.string.daily_hint_2, R.string.daily_hint_3)
private var xp = intArrayOf(R.mipmap.tenxp_round, R.mipmap.fiftyxp_round, R.mipmap.hundredxp_round)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.task_layout, parent, false)
return ViewHolder(v)
}
override fun getItemCount(): Int {
//cant use context here because no value is passed for parameter
return dailyTasks.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//var context = holder.itemView.context -> this works here but not as a constructor
if(context is ToDoList) {
holder.itemImage.setImageResource(xp[0])
holder.itemDetail.text = dailyTasks[position].toString()
}
else{
holder.itemImage.setImageResource(xp[1])
holder.itemDetail.text = weeklyTasks[position].toString()
}
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
var itemImage: ImageView
var itemDetail: TextView
init {
itemImage = itemView.findViewById(R.id.item_image)
itemDetail = itemView.findViewById(R.id.item_detail)
}
}
}
Related
This question already has answers here:
How to get a context in a recycler view adapter
(13 answers)
Closed 23 days ago.
I have a RecyclerView adapter for displaying a list of movies. I need to use the context to initialize the genrePreferences variable in my adapter. Where is the appropriate place in the adapter's lifecycle to initialize this variable?
class MovieAdapter(private val movieList: ArrayList<Movie>) :
RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
private val BASE_POSTER_PATH = "https://image.tmdb.org/t/p/w342"
lateinit var genrePreferences: GenrePreferences
lateinit var genres: HashMap<Int, String>
class MovieViewHolder(var view: ItemMovieTvshowBinding, val context: Context) :
RecyclerView.ViewHolder(view.root) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate<ItemMovieTvshowBinding>(
inflater,
R.layout.item_movie_tvshow,
parent,
false
)
return MovieViewHolder(view, parent.context)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
//...
}
override fun getItemCount(): Int {
return movieList.size
}
}
You can make a constructor in your adapter and pass the context by it. Or you can define a static contex in Application class and use it everywhere.
In common case you shouldn't use context as a local variable.
You can access context inside onBindViewHolder, onCreateViewHolder, where you get views (binding).
But for your task you can add some variable like private var context: Context? = null or private var isGenreInitialized = false, then initialize genrePreferences for the first time when you get a view.
Don't use lateinit in almost all situations, it can lead to different bugs.
Or you can pass context via class constructor:
class MovieAdapter(private val context: Context, private val movieList: ArrayList<Movie>)
I am building an Android app with Kotlin and decided to replace the calls to findViewById and use binding. It all works fine but specifically, when I change an Adapter for a RecyclerView it breaks the item layout.
Original code with findViewById:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_weight, parent, false)
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = this.itemView.findViewById(R.id.weightValue)
private val weightDateView: TextView = this.itemView.findViewById(R.id.weightDate)
private val weightImageView: ImageView = this.itemView.findViewById(R.id.weightImage) as ImageView
And this is the layout:
But then whenever I use binding:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
private var _binding: ListItemWeightBinding? = null
private val binding get() = _binding!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
_binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = binding.weightValue
private val weightDateView: TextView = binding.weightDate
private val weightImageView: ImageView = binding.weightImage
The layout breaks:
Any ideas about what am I doing wrong here? Is it a bug?
P.S - For now, I am just adding the annotation to ignore bindings as documented here for the item view but I would really like to understand what's wrong.
Your binding needs to be inflated in the context of its parent so its root view's layout params will take effect:
binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
I think it will also give you problems that you are creating a binding property for the Adapter if you try to use it long term. Each ViewHolder holds a distinct view with a distinct binding instance. It's working now because you use it only for the ViewHolder instantiation immediately after setting each instance. But if that's all your intent is, you should just pass the binding to the constructor of your ViewHolder and omit the adapter's property.
By the way, this is the sort of pattern I use for ViewHolders. Less code. Note, it doesn't have to be an inner class.
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views here
}
}
}
I agree with #Tenfour04, using the same instance of binding is wrong but I believe the root cause of your issue is with the binding logic. with binding, the data is bound to bind with the view but not immediately. So your view gets inflated but since the binding happens at a later stage, scheduled to happen in near future, the item_view width is shrunk.
So try the following,
// oncreate view logic
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return WeightHolder(binding)
}
// onBindViewHolder logic remains the same
// this remains same as suggested by #Tenfour04 but a change in the bind function
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views using databindig
customVariable = item
executePendingBindings() // this is important
}
}
}
// define the customvariable in your `item_list_view.xml`
<variable
name="customVariable"
type="packagename.WeightWithPictures" />
executePendingBindings() is the way we force the binding to happen right away and not to schedule it later
Edit:
This answer is from Databinding perspective and not ViewBinding
I am trying to pass an array from my Recyclerview Activity to its Adapter as such:
//Setting NavBar Title
val navBarTitle = intent.getStringExtra(FirstCustomViewHolder.LESSON_TITLE_KEY)
supportActionBar?.title = navBarTitle
var content : Array<String>
if (navBarTitle == "Introduction"){
content = arrayOf("Intro1", "Intro2")
}
else{
content = arrayOf(":esson1-1", "Lesson 1-2")
}
I am passing the array as such:
recyclerView_main.adapter = SecondAdapter(content)
And I am getting an angry red underline as shown below.
On mouse-over the pop-up error reads:
Too many arguments for public constructor......
Is there a proper way to pass an array or variable to my adapter? I am fairly new to Kotlin and appreciate and pointers.
Thank you.
Edit:
As requested, this is my adapter class:
class SecondAdapter : RecyclerView.Adapter<SecondCustomViewGolder>(){
//Variable below to be replaced by array from Activity
var lessons = arrayOf("Satu", "Dua", "Tiga", "Empat", "Lima", "Enam", "Tujuh",
"Lapan", "Sembilan")
override fun getItemCount(): Int {
return lessons.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SecondCustomViewGolder {
var layoutInflater = LayoutInflater.from(parent.context)
var cellForRow = layoutInflater.inflate(R.layout.lesson_row, parent, false)
return SecondCustomViewGolder(cellForRow)
}
override fun onBindViewHolder(holder: SecondCustomViewGolder, position: Int) {
}
}
class SecondCustomViewGolder(var viewTwo : View) : RecyclerView.ViewHolder(viewTwo){
}
Does your SecondAdapter class constructor accept an Array as an argument? If not, you must add it there. The error is because you're trying to pass an argument to a constructor that accepts no arguments.
EDIT
Do it like so:
class SecondAdapter(val lessonArray: Array<String>) : RecyclerView.Adapter<SecondCustomViewGolder>(){
override fun getItemCount(): Int {
return lessons.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SecondCustomViewGolder {
var layoutInflater = LayoutInflater.from(parent.context)
var cellForRow = layoutInflater.inflate(R.layout.lesson_row, parent, false)
return SecondCustomViewGolder(cellForRow)
}
override fun onBindViewHolder(holder: SecondCustomViewGolder, position: Int) {
}
}
class SecondCustomViewGolder(var viewTwo : View) : RecyclerView.ViewHolder(viewTwo){
}
I made it a val since it's my preference. If you intend to modify the variable, than you just declare it as a var in the constructor. There's no need to assign it inside the class. Just declaring it in the constructor makes it accessible throughout the class.
You can use the ListAdapter
and use submitList()
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?) {
...
}
This question already has answers here:
Check which type of object List<?> contains
(4 answers)
Closed 1 year ago.
I am trying to use a single RecyclerView.Adapter to view multiple Arraylists of different object models in multiple fragments.
But I don't know how to do it, is there a way to do it and if possible how can I do it?
I tried to use List object and cast it to the object models I wanted. But I don't know how to use them in onBindViewHolder, I don't know if this is even a proper method.
public RItemAdapter(List<?> item,String tag) {
if(tag.equals("battery"))
{
bList = (List<BatteryModel>) item;
}
else if(tag.equals("device"))
{
dList = (List<DeviceModel>) item;
}
else if(tag.equals("network"))
{
nList = (List<NetworkModel>) item;
}
else if(tag.equals("storage"))
{
sList = (List<StorageModel>) item;
}
else if(tag.equals("weather"))
{
wList = (List<WeatherModel>) item;
}
}
I want to populate multiple RecyclerViews in multiple Fragments with a single adapter.
You can make a base class, other class will inherit from it. You can have a list of the object you want in the inherited classes: each cell will contain a data object that contain a list of other stuff you want to display.
Then in your adapter you have only one list of your base class, and you will set the view type of your object at a position in the getItemViewType method of your adapter.
In the onBindViewHolder, just switch case on the view type and bind the right ViewHolder to the case.
Here is an example code in Kotlin:
class TestAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val VIEW_TYPE_ONE = 0
private const val VIEW_TYPE_TWO = 1
}
private val data = mutableListOf<Data>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder = when (viewType) {
VIEW_TYPE_ONE -> ViewHolderOne.create(parent)
VIEW_TYPE_TWO -> ViewHolderTwo.create(parent)
else -> throw IllegalStateException("Viewtype $viewType unhandled")
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolderOne -> {
holder.bind(data[position] as DataOne)
}
is ViewHolderTwo -> {
holder.bind(data[position] as DataTwo)
}
}
}
override fun getItemViewType(position: Int): Int = when (data[position]) {
is DataOne -> VIEW_TYPE_ONE
is DataTwo -> VIEW_TYPE_TWO
}
override fun getItemCount(): Int = data.count()
override fun getItemId(position: Int): Long = data[position].id
fun setData(newData: List<MessageUiModel>) {
data.clear()
data.addAll(newData)
notifyDataSetChanged()
}
}
sealed class Data
data class DataOne(val list: List<Any>): Data()
data class DataTwo(val list: List<Any>): Data()