For a school project, I made a custom arrayadapter with a context, resources and items attribute. Today I received feedback from my teacher and he wants me to find a solution where I don't have a Context attribute because he doesnt like that I always need to specify the context.
This is my code:
class TalentListAdapter(
var mCtx: Context,
var resources: Int,
var items: MutableList<Talent>
) : ArrayAdapter<Talent>(mCtx, resources, items) {
lateinit var mItem: Talent
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layoutInflater: LayoutInflater = LayoutInflater.from(mCtx)
val view: View = layoutInflater.inflate(resources, null)
mItem = items[position]
//set name of talent
val talentTextView: TextView = view.findViewById(R.id.talentName)
talentTextView.text = mItem.toString()
return view
}
}
He wants to get rid of the mCtx: Context attribute, but I don't find a solution for it. Any suggestions?
The adapter is created like this atm:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listView: ListView = binding.talentList
// set custom adapter for talent_list
val adapter = TalentListAdapter(view.context, R.layout.talentlayout, binding.talentViewModel?.getTalents()?.value as MutableList<Talent>)
listView.adapter = adapter
}
Can you try this
class YourAdapter() : RecyclerView.Adapter<YourAdapter.ViewHolder>() {
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
var Data: List<YourResponse> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.your_layout, parent, false
)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return Data.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val getYourModel = Data[position]
val Binding = holder.view
Binding.yourTextView.apply{
text = getYourModel.yourField
}
.....
}
}
and pass Data from your Fragment or Activity (my example uses Retrofit2)
private var theList: List<YourResponse> = emptyList()
private val theAdapter =yourAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onUpdateData()
}
private fun onUpdateData() {
...
override fun onResponse(call: Call<YourResponse>, response: Response<YourResponse>) {
val body = response.body()?.results
if (response.isSuccessful && body != null) {
onUpdateDataSuccess(body)
} else {
// Something
}
}
...
}
private fun onUpdateDataSuccess(data: List<YourResponse>) {
theList = data
theAdapter.Data = data
}
...
You only need context while inflating so you should use parent.context instead of explicit context.
Parent.Context represent the Activity/Fragment where this recycler view is implemented.
Related
I'm trying to refactor my app to use ViewBinding. I've gone through all the fragments and activities; however, I have an ArrayAdapter that I'm unsure of the proper convention to use view binding to prevent memory leaks.
What is the proper way to use a viewbinding in an ArrayAdapter?
I have been using this method for fragments:
private var _binding: BINDING_FILE_NAME? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = BINDING_FILE_NAME.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
I call my adapter like so:
var myadapter : MyCustomAdapter = MyCustomAdapter(requireContext(), R.layout.row_autocomplete_item, myListOfStrings())
MyCustomAdapter class
class MyCustomAdapter(ctx: Context, private val layout: Int, private val allItems: List<String>) : ArrayAdapter<String>(ctx, layout, allItems) {
var filteredItems: List<String> = listOf()
override fun getCount(): Int = filteredItems.size
override fun getItem(position: Int): String = filteredItems[position]
#SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context).inflate(layout, parent, false)
val item = filteredItems[position]
view.apply {
// HERE IS WHERE I AM NEEDING TO BIND THE VIEW
tvName?.text = item
}
return view
}
override fun getFilter(): Filter {
return object : Filter() {
override fun publishResults(charSequence: CharSequence?, filterResults: FilterResults) {
#Suppress("UNCHECKED_CAST")
filteredItems = filterResults.values as List<String>
notifyDataSetChanged()
}
override fun performFiltering(charSequence: CharSequence?): FilterResults {
val queryString = charSequence?.toString()?.lowercase(Locale.ROOT)
val results = FilterResults()
results.values = if (queryString == null || queryString.isEmpty())
allItems
else
allItems.filter {
it.lowercase(Locale.ROOT).contains(queryString)
}
return results
}
}
}
}
I did like this, its working. But Im not sure, whether it is correct way or not
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding: LayoutCustomSpinnerBinding
var row = convertView
if (row == null) {
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
binding = LayoutCustomSpinnerBinding.inflate(inflater, parent, false)
row = binding.root
} else {
binding = LayoutCustomSpinnerBinding.bind(row)
}
binding.txtContent.text = spinnerList[position].ValueData
return row
}
Based on this answer, got this:
If convertView is not null, then bind to it. Inflate otherwise.
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding: MyLayoutBinding =
if (convertView != null) MyLayoutBinding.bind(convertView)
else MyLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
// use binding
val item = getItem(position)
binding.text = item.name
return binding.root
}
class HoursAdapter(private val hoursList: List<HoursItem>)
:RecyclerView.Adapter<HoursAdapter.HoursViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
HoursViewHolder {
val binding = HoursListItemsBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return HoursViewHolder(binding)
}
override fun getItemCount() = hoursList.size
override fun onBindViewHolder(holder: HoursViewHolder, position: Int) {
with(holder){
with(hoursList[position]) {
binding.topLearnerName.text = name
val hours = "$hours learning hours, $country"
binding.topLearnerTime.text = hours
GlideApp.with(holder.itemView.context)
.load(badgeUrl)
.into(binding.topLearnerImage)
holder.itemView.setOnClickListener {
Toast.makeText(holder.itemView.context, hours,
Toast.LENGTH_SHORT).show()
}
}
}
}
inner class HoursViewHolder(val binding: HoursListItemsBinding)
:RecyclerView.ViewHolder(binding.root)
}
I'm trying to pass my binding variable for the adapter to the ViewHolder to assing values to the textviews in my adapter but the values are not assigned and the click listeners dont do anything.
Here's my adapter class:
class DoneAppointmentsAdapter(var context: DoneAppointmentsFragment, listener: ContentListener, var arrayList: List<Appointment>) :
RecyclerView.Adapter<DoneAppointmentsAdapter.ItemHolder>() {
private val listener: ContentListener = listener
private var binding: DoneAppointmentsAdapterBinding? = null
var activity = context
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ItemHolder {
val viewHolder = LayoutInflater.from(parent.context)
.inflate(R.layout.done_appointments_adapter, parent, false)
binding = DoneAppointmentsAdapterBinding.inflate(LayoutInflater.from(parent.context))
return ItemHolder(viewHolder,binding, parent.context)
}
override fun onBindViewHolder(holder: ItemHolder, position: Int) {
holder.bind(arrayList, listener)
}
override fun getItemCount(): Int {
return arrayList.size
}
class ItemHolder(view: View, var binding: DoneAppointmentsAdapterBinding?, val context: Context) : RecyclerView.ViewHolder(view) {
fun bind(listOfData: List<Appointment>, listener: ContentListener) {
val dataListItem = listOfData[adapterPosition]
binding?.donePatientItemName?.text = "${dataListItem.patientName}"
binding?.donePatientItemTime?.text = "Some date"
binding?.donePatientItemPatientInfo?.setOnClickListener {
tempAppointmentId = dataListItem.id
listener.onPatientHistoryViewClicked(dataListItem.requestedBy)
}
binding?.donePatientItemReviewCase?.setOnClickListener {
amendPrescriptionWarning(dataListItem,context)
}
binding.donePatientItemViewCase.setOnClickListener {
startPreview(dataListItem)
}
}
}
}
You have messed up inside onCreateViewHolder . you have created view twice once with the LayoutInflater.from(parent.context) and once with DoneAppointmentsAdapterBinding.inflate . There should be only one in this case onlyDoneAppointmentsAdapterBinding .
class DoneAppointmentsAdapter(var context: DoneAppointmentsFragment, listener: ContentListener, var arrayList: List<Appointment>) :
RecyclerView.Adapter<DoneAppointmentsAdapter.ItemHolder>() {
private val listener: ContentListener = listener
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ItemHolder {
val binding = DoneAppointmentsAdapterBinding.inflate(LayoutInflater.from(parent.context))
return ItemHolder(binding)
}
override fun onBindViewHolder(holder: ItemHolder, position: Int) {
holder.bind(arrayList, listener)
}
override fun getItemCount(): Int {
return arrayList.size
}
class ItemHolder (var binding: DoneAppointmentsAdapterBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listOfData: List<Appointment>, listener: ContentListener) {
val dataListItem = listOfData[adapterPosition]
binding.donePatientItemName.text = "${dataListItem.patientName}"
binding.donePatientItemTime.text = "Some date"
binding.donePatientItemPatientInfo.setOnClickListener {
tempAppointmentId = dataListItem.id
listener.onPatientHistoryViewClicked(dataListItem.requestedBy)
}
binding.donePatientItemReviewCase.setOnClickListener {
amendPrescriptionWarning(dataListItem,binding.donePatientItemReviewCase.context)
}
binding.donePatientItemViewCase.setOnClickListener {
startPreview(dataListItem)
}
}
}
}
It should be like this . I have also removed extra useless arguments from ItemHolder .
To make it clear the problem with your code was RecyclerView.ViewHolder(view) you were passing view while you were doing all action and stuff on binding those holds are two different instances of View . Which should be fixed with above code.
I have a Data Repository that saved data & fetches data, this is passed into a ViewModel as constructor. I have methods in a ViewModel that fetches and saves data.
I have a button that I click for each row (Item in a RecyclerView list) this saves data using the ViewModel.
I have found that I can directly call a ViewModel initialised it into the constructor, I checked the Google Android examples & this part is not covered.
Something like this below:
Copied from: Databinding Recyclerview and onClick
private ExampleViewModel exampleViewModel;
public ExampleListAdapter(Context context, List<Model> models) {
this.context = context;
this.models = models;
// ...
exampleViewModel = ViewModelProviders.of((FragmentActivity) context).get(ExampleViewModel.class);
}
But then, I could also call a ViewModel by passing a ViewModel object from the Activity alongside with the context.
So what the proper way of calling a ViewModel?
This is your pojo class
data class Item(val id: Int)
This is your adapter.
class Adapter : RecyclerView.Adapter<Adapter.ViewHolder>() {
var items: List<Item> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
var callback: Callback? = null
override fun getItemCount(): Int {
return items.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.simple_textview, parent, false)
return ViewHolder(itemView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.setOnClickListener {
callback?.onItemClicked(item)
}
holder.bindItem(item)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItem(item: Item) {
// Fill layout
}
}
interface Callback {
fun onItemClicked(item: Item)
}
}
This is your viewModel class.
class MyViewModel : ViewModel(), Adapter.Callback {
override fun onItemClicked(item: Item) {
}
}
And this is your fragment.
class MyFragment : Fragment() {
private val adapter = Adapter()
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
adapter.callback = myViewModel
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter.items = listOf(
Item(1),
Item(2)
)
//Setup recyclerView etc.
}
}
I am using MVVM with Room persistence and livedata. I am fetching the data from local database and want to show in the form of list in the recyclerView and my recyclerView is not showing anything.
My adapter is like any other regular adapter
RecyclerView Code
class MyInformationAdapter : RecyclerView.Adapter<MyInformationAdapter.ViewHolder>() {
private var myList: List<PersonInformation> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.my_adapter_data, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = myList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var myDataClass: PersonInformation = myList[position]
holder.name.text = myDataClass.name
holder.fName.text = myDataClass.fatherName
holder.email.text = myDataClass.email
holder.contact.text = myDataClass.contactNo.toString()
}
fun updateTheState(myList: List<PersonInformation>){
this.myList = myList
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var name: TextView = itemView.findViewById(R.id.yourName)
var fName: TextView = itemView.findViewById(R.id.yourFatherName)
var email: TextView = itemView.findViewById(R.id.yourEmail)
var contact: TextView = itemView.findViewById(R.id.yourContact)
}
}
RecyclerView Activity Code
class FinalActivity : AppCompatActivity() {
private var myDataList : List<PersonInformation> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_final)
setAdapter()
}
private fun setAdapter() {
val adapter = MyInformationAdapter()
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
adapter.updateTheState(myDataList)
}
}
*Fragment as a View of MVVM *
class PersonalInformationFragment : Fragment() {
private var viewModel: PersonInformationViewModel? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_personal_information, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
btn.setOnClickListener {
viewModel = ViewModelProviders.of(this)[PersonInformationViewModel::class.java]
viewModel?.getAllData()?.observe(this, Observer {
this.setAllData(it)
})
val intent = Intent(activity, FinalActivity::class.java)
startActivity(intent)
}
}
private fun setAllData(personInformation: List<PersonInformation>) {
val setData = PersonInformation(
name.text.toString(),
fName.text.toString(),
email.text.toString(),
contact.text.toString().toInt()
)
viewModel?.setTheData(setData)
}
}
I want to set an onClick Listener to the imageView present in the recycler view. But whenever I pass the imageview from onViewCreated() method from the fragment, it is still null and throws a NullPointerException when I invoke setOnClickListener.
These are kotlin classes.
class ShowDuesFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_show, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = Adapter(mutableListOf(..), fragmentmanager!!, imageView)
recycler_view.apply {
layoutManager = LinearLayoutManager(activity!!.applicationContext)
setHasFixedSize(true)
this.adapter = adapter
}
}
}
class Adapter(private val list: List<Due>, private val manager: FragmentManager, private val imageView: ImageView?) : RecyclerView.Adapter<ViewHolder> {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.card_view,
parent,
false
),
parent.context,
manager,
imageView
)
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.putData(list[position])
}
class ViewHolder(itemView: View, context: Context, manager: FragmentManager, imageView: ImageView? = null) :
RecyclerView.ViewHolder(itemView), DatePickerDialog.OnDateSetListener {
lateinit var item: Due
init {
Log.i("ViewHolder", (imageView == null).toString()) //log prints 'true'
imageView?.setOnClickListener {
val popup = PopupMenu(context, it)
popup.menuInflater.inflate(R.menu.menu_popup, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.notify -> {
val datePicker = DatePickerFragment()
datePicker.show(manager, "DatePickerFragment")
}
}
context.toast((item as Due).name)
true
}
popup.show()
}
}
fun putData(due: Due) {
…
item = due
}
…
}
The log message I get is true and when I click on the imageView, it does not respond to my clicks. How do I successfully implement an onClickListener to my imageView?
You are never initializing the imageView. As you are using ( ?.) while setting onClickListner to ImageView, you are not getting any crash due to safe call. One better way to do this is:
class ShowDuesFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_show, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = Adapter(mutableListOf(..), fragmentmanager!!)
recycler_view.apply {
layoutManager = LinearLayoutManager(activity!!.applicationContext)
setHasFixedSize(true)
this.adapter = adapter
}
}
}
class Adapter(private val list: List, private val manager: FragmentManager, private val imageView: ImageView?) : RecyclerView.Adapter {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.card_view,
parent,
false
),
parent.context,
manager
)
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.putData(list[position])
}
class ViewHolder(itemView: View, context: Context, manager: FragmentManager) :
RecyclerView.ViewHolder(itemView), DatePickerDialog.OnDateSetListener {
lateinit var item: Due
init {
val imageView = itemView //Cast this to image view if required you can use itemView.findViewById for other //views.
Log.i("ViewHolder", (imageView == null).toString()) //log prints 'true'
imageView?.setOnClickListener {
val popup = PopupMenu(context, it)
popup.menuInflater.inflate(R.menu.menu_popup, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.notify -> {
val datePicker = DatePickerFragment()
datePicker.show(manager, "DatePickerFragment")
}
}
context.toast((item as Due).name)
true
}
popup.show()
}
}
fun putData(due: Due) {
…
item = due
}
…
}
should work if you change onViewCreated to:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = Adapter(mutableListOf(..), fragmentmanager!!, view.imageView) // Specify which imageView you mean, as there might be a bunch of these fragments or none, system doesn't know.
or, do the whole view.findViewById() thingy if you're not using the kotlinx thingies
The ViewHolder class should be inside the Adapter as an inner class. By doing so, imageView is not null.
Something like this:-
class Adapter (
private val list: List<Due>,
private val manager: FragmentManager
) : RecyclerView.Adapter<Adapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.card_view,
parent,
false
),
parent.context
)
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.putData(list[position])
inner class ViewHolder(itemView: View, context: Context) :
RecyclerView.ViewHolder(itemView) {
private lateinit var item: Due
init {
Log.i("ViewHolder", (imageView == null).toString()) //log prints 'false'
itemView.dropdown_menu.setOnClickListener {
val popup = PopupMenu(context, it)
popup.menuInflater.inflate(R.menu.menu_popup, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.notify -> {
val datePickerFragment = DatePickerFragment()
datePickerFragment.show(manager, "date picker")
}
}
true
}
popup.show()
}
}
fun putData(due: Due) {
…
}
}
}
And we instantiate the Adapter like:-
val adapter = Adapter(mutableListOf(…), fragmentmanager!!)