I want to make a custom view with two Recyclerview data. First one is header titles, second one is content details. When I click on a title, I want different XML files to be shown.
After some research, I saw that I could do it using TabLayout and ViewPager, however, I don't want to use an extra fragment for each tab, instead I want to inflate an XML file. Because I want to use this view on other screens as a custom view.
How can I do that? What should I use as a solution? Thank you.
You can use ViewPager2 :
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/topicViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
you can assign second recycler adapter to ViewPager2 for example:-
class DummyAdapter(var list:ArrayList<String>) :
RecyclerView.Adapter<DummyAdapter.ViewHolder>() {
lateinit var context: Context
var isFullScreen = false
var isDownload: Boolean = false
var doubleClick = false
inner class ViewHolder(val binding: DummyItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
position: Int,
context: Context,
adapter: DummyAdapter,
) {
with(binding) {
// add data
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder {
context = parent.context
val inflater = LayoutInflater.from(context)
val binding = DummyItemBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(position, context, this, list[position])
override fun getItemCount(): Int {
return list.size
}
}
binding.viewPager.adapter = dummyAdapter
then on the click of TabLayout you can update the adapter of second recyclerView.
Related
I am new to Kotlin and/or coding. The below mentioned are the code I made to populate a List View. But as I run the code, the activity_main.xml file is inflated but the text data are not attached.
Datasource.kt
class Datasource{
companion object{
val affirmationList: List<String> = listOf(
"Affirmation_1", "Affirmation_2", "Affirmation_3", "Affirmation_4")}
}
Here is the CustomAdapter.kt,
class CustomAdapter(val mContext:Context, val layoutId:Int, val textId: Int, val x: List<String>) :ArrayAdapter<Datasource>(mContext, layoutId, textId) {}
Here is MainActivity.kt
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Item.View is the Id of ListView in activity_main.xml
val itemListView= findViewById<ListView>(R.id.Item_View)
// item_list is the Id of item_list.xml & Affirmation_Text is the Id of TextView in item_list.xml
val customAdapterUse = CustomAdapter(this,R.layout.item_list, R.id.Affirmation_Text, Datasource.affirmationList)
itemListView.adapter = customAdapterUse
}
}
You need to learn how the recycler view works. You haven't implemented any of the RecylerView adapter functions. I would recommend you watch some YouTube videos or tutorials on how to populate a list using RecyclerView. After you understand how the recycler view works, you can get some help from the documentation here.
Here is a code sample to give you an idea as to how your recycler view adapter should look like:
class CustomAdapter(private val dataSet: Array<String>) :
RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView
init {
// Define click listener for the ViewHolder's View.
textView = view.findViewById(R.id.textView)
}
}
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.text_row_item, viewGroup, false)
return ViewHolder(view)
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
// Get element from your dataset at this position and replace the
// contents of the view with that element
viewHolder.textView.text = dataSet[position]
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount() = dataSet.size
}
Thanks for all of your feedback. I worked it out. Just changed CustomAdaptder class (added String type for ArrayAdapter and added data source variable x to the ArrayAdapter constructor. The final code is as below:
CustomAdapter(val mContext:Context, val layoutId:Int, val textId: Int, val x: List<String>) :ArrayAdapter<String>(mContext, layoutId, textId, x) {}
The code, which was not attaching data:
class CustomAdapter(val mContext:Context, val layoutId:Int, val textId: Int, val x: List<String>) :ArrayAdapter<Datasource>(mContext, layoutId, textId) {}
I have a recycler view(Parent) and inside it, I have another recycler View (Child).
There are 2 operations in child recycler View which I want to get on Fragment Class and do some things dynamically.
Architecture: MVVM
Yes, you can achieve your desired behavior by following these steps:
I will use Lambda to refer to Higher Order Function.
Pass the Lambda function from Activity/Fragment -> Parent Adapter
Pass the Lambda function from Parent Adapter -> Child Adapter.
For example, this code shows how to get a callback from nested Recyclerview when a user clicks Error Item from child Recyclerview.
//In Activity/Fragment
private var errorClick: () -> Unit
parentAdapter.setErrorClick(errorClick)
//In Parent Adapter
private var errorClick: () -> Unit
childAdapter.setErrorClick(errorClick)
//In Child Adapter - Now use errorClick to callback methods to Activity/Fragment
private var errorClick: () -> Unit // Use IT!
Let me know if you have any questions. Thanks.
Here is an example for higher order function.
TextAdapter.kt class
class TextAdapter(
val onClick: (String) -> Unit
): RecyclerView.Adapter<TextAdapter.ViewHolder>() {
inner class ViewHolder(val binding: ItemTextBinding) :
RecyclerView.ViewHolder(binding.root)
private val list: ArrayList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder =
ViewHolder(
ItemTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val adapter = InnerAdapter {
onClick.invoke(it)
}
binding.recyclerViewList.adapter = adapter
binding.recyclerViewList.setData(list)
}
override fun getItemCount(): Int = list.size
fun setData(newList: ArrayList<String>) {
list.clear()
list.addAll(newList)
}
}
Inner adapter
class InnerAdapter(
val onClick: (String) -> Unit
): RecyclerView.Adapter<InnerAdapter.ViewHolder>() {
inner class ViewHolder(val binding: ItemText1Binding) :
RecyclerView.ViewHolder(binding.root)
private val list: ArrayList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
ViewHolder =
ViewHolder(
ItemText1Binding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
binding.tvTxt.text = list[position]
holder.itemView.setOnClickListener {
//change background color
onClick.invoke(list[position])
}
}
override fun getItemCount(): Int = list.size
fun setData(newList: ArrayList<String>) {
list.clear()
list.addAll(newList)
}
}
Now you can get the value of the item click on the setAdapter
Let's see how
The below function is called from the fragment class where the adapter is set
val adapter = TextAdapter {
showToast(it)
}
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 using recyclerView to show data from firebase database and I want to handle clicks,
Now the important part is that I want to know the number that was clicked in order to test google play in app billing before showing the next activity
I mean user should click item number one then pay to see information number 1 and so on
Any help, please ?
//my adapter
class MyAdapter(
private val arrayList: ArrayList<Long>
) :
RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_item, parent, false)
return MyViewHolder(view)
}
override fun getItemCount() = arrayList.size
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.number.text = arrayList[position].toString()
}
class MyViewHolder(view: View) :
RecyclerView.ViewHolder(view) {
val number = view.findViewById<View>(R.id.singleNumberId) as TextView
}
}
Here is a small example I have of registering a click for a RecyclerView adapter item:
class PatientListAdapter : ListAdapter<Patient, PatientListAdapter.PatientViewHolder>(co.za.abcdefgh.viewmodels.PatientListViewModel.DiffItemCallback) {
// this property will be used to set the onclick callback for the entire adpater
var onPatientSelectedCallback: PatientSelectedCallback? = null
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PatientViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_patient, parent, false) as View
return PatientViewHolder(view)
}
override fun onBindViewHolder(holder: PatientViewHolder, position: Int) {
holder.view.item_patient_name.text = getItem(position).toString()
holder.view.item_patient_folderNumber.text = getItem(position).folderNumber
// lets set our on click for each viewholder here
holder.view.item_patient_info_card.setOnClickListener {
// the secret sauce .... getItem(holder.adapterPosition)
onPatientSelectedCallback?.onPatientSelected(getItem(holder.adapterPosition))
}
}
class PatientViewHolder(val view: View) : RecyclerView.ViewHolder(view)
// interface which defines a method signature that will called when a item in the adpater is selected
interface PatientSelectedCallback {
fun onPatientSelected(patient: Patient)
}
}
and then wherever you use the adapter after instantiating simply do:
val viewAdapter = PatientListAdapter()
viewAdapter.onPatientSelectedCallback =
object : PatientListAdapter.PatientSelectedCallback {
override fun onPatientSelected(patient: Patient) {
// do something with the chosen item
patientViewModel.setPatient(patient)
}
}
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()