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>)
Related
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)
}
}
}
enter image description here
I AM trying to create other constructor in recyclerView adapter but getting error
please tell me how to create secondary constructor in recycler adapter taking arraylist parameter in kotlin android studio
class CustomRecyclerAdapter constructor(
val context1: Context,
val topics: Array<String>,
private var alist: ArrayList<Array<String>>
) : RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolder>() {
constructor(sss: ArrayList<Array<String>>, ttt: Array<String>) : this(sss) {
this.alist = sss
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CustomRecyclerAdapter.ViewHolder {
val layout = LayoutInflater.from(parent.context).inflate(R.layout.cardlayout, parent, false)
return ViewHolder(layout)
}
override fun onBindViewHolder(holder: CustomRecyclerAdapter.ViewHolder, position: Int) {
holder.topicTextView.text = topics[position]
}
override fun getItemCount(): Int {
return topics.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var topicTextView: TextView = itemView.findViewById(R.id.gir_topics_tvid)
}
}
Please use these constructors:
class CustomRecyclerAdapter constructor(
val context: Context,
val topics: Array<String> = emptyArray(),
private var alist: ArrayList<String>
) : RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolder>() {
constructor(context: Context, sss: ArrayList<String>) : this(context, alist = sss)
// ...
}
In the primary constructor you need to specify a default value for topics. Also add context: Context parameter to the secondary constructor and pass it to this() when calling the primary constructor.
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 have an Android project in MVVM Structure. In that project, consist RecyclerView. This is my code.
1.MyViewModel.kt
class MyViewModel(context: Application, val myRepository: MyRepository) : AndroidViewModel(context), Observable{
... other code ...
val listUri: MutableLiveData<MutableList<Uri>> by lazy {
MutableLiveData<MutableList<Uri>>().apply {
value = mutableListOf()
}
}
... other code ...
}
2.MyAdapter.kt
class MyAdapter(var items: MutableList<Uri>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var listItems: ArrayList<Uri> = items as ArrayList<Uri>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.pod_list_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.imageIcon.setImageURI(listItems[position])
holder.deleteIcon.setOnClickListener {
val viewModel: MyViewModel? = null
....THIS PART....
viewModel?.listUri?.value.removeAt(position)
....UNTIL THIS PART....
}
}
override fun getItemCount(): Int {
return editedItems.size
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var imageIcon = view.icon_image
var deleteIcon = view.icon_delete
}
When user click the delete icon in the recyclerview item, the application suppose to delete the selected value of MutableLiveData<MutableList<Uri>> in MyViewModel which name listUri.
In my marked code above, when i debug the code, it return null value, even though the variable listUri in ViewModel have value.
So i think, my way to access the value of ViewModel from Adapter is wrong.
How to access value of ViewModel from the adapter and then manipulate it?
If can't do that, any suggestion would be nice.
you can access your ViewModel from the context which you are getting into the adapter.
I guess instead of passing the context in the constructor, pass the ViewModel instead:
class MyAdapter(var items: MutableList<Uri>, val viewModel: MyViewModel) : RecyclerView.Adapter<ViewHolder>()
Then instead of:
holder.deleteIcon.setOnClickListener {
val viewModel: MyViewModel? = null
....THIS PART....
viewModel?.listUri?.value.removeAt(position)
....UNTIL THIS PART....
}
You can do:
holder.deleteIcon.setOnClickListener {
viewModel.listUri.value?.removeAt(position)
}
I hope this helps
I am trying to observe fields of my class without exposing it. So far, I've tried this:
TaskItemViewModel.kt
open class TaskItemViewModel(private val navigator: ITaskNavigator) : ViewModel() {
private val taskItem: MutableLiveData<TaskItem> = MutableLiveData()
val title: LiveData<String?> = Transformations.map(taskItem) { it.title }
val content: LiveData<String?> = Transformations.map(taskItem) { it.content }
var showCheck: LiveData<Boolean> = Transformations.map(taskItem) { it.isCompleted }
fun setModel(model: TaskItem) {
this.taskItem.value = model
}
}
ItemListScreenAdapter.kt
class ItemListScreenAdapter(private val navigator: ITaskNavigator) : RecyclerView.Adapter<ItemListScreenAdapter.TaskItemViewHolder>() {
private val TAG = "ItemListScreenAdapter"
private var dataset: List<TaskItem> = listOf()
override fun onBindViewHolder(viewHolder: TaskItemViewHolder, position: Int) {
with(viewHolder.binding) {
this.viewModel?.setModel(dataset[position])
executePendingBindings()
}
}
fun updateDataset(dataset: List<TaskItem>) {
Log.d(TAG,"Updating dataset")
this.dataset = dataset
notifyDataSetChanged()
}
override fun getItemCount(): Int = dataset.size
override fun onCreateViewHolder(parent: ViewGroup, type: Int): TaskItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemTaskBinding.inflate(inflater, parent, false)
binding.viewModel = TaskItemViewModel(navigator)
return TaskItemViewHolder(binding)
}
class TaskItemViewHolder(val binding: ItemTaskBinding) : RecyclerView.ViewHolder(binding.root)
}
If I call setModel before inflating the view, everything works fine. However, after the view is inflated, the view is not updated even if taskItem 's value is updated. You can be assured that updateDataset is called everytime there is a change in dataset.
I want the view to be updated whenever I call setModel in corresponding viewmodel. What are the ways to achieve this?
For this viewmodel, I want to use ViewModel rather than BaseObservable. Therefore, please give your answers according to this.
EDIT:
I have found the solution to the problem.
in ItemListScreenAdapter's onCreateViewHolder method, after inflating, I needed to set LifeCycleOwner of the binding.
I added the following line after inflating the ItemTaskBinding.
binding.setLifecycleOwner(parent.context as MainActivity)
and the problem is solved and view is being updated.