I am fetching the categories from an API and I would like to fire up a new fragment by passing the category name to retrofit on the button click. I am thinking of doing it in my UI with getText()
on the button clicked and firing up the API call which will display the result on a new fragment.
The API I am using is https://fakestoreapi.com/
To my understanding, I have to handle the click events in the UI part (Fragment's Code).
The code below is in my FakeApiService.kt
@GET("categories")
suspend fun getCategories(): List<String>
@GET("categories/{product}")
suspend fun getProduct(@Path(value = "product", encoded = true) productType: String): List<Product>
However, I am having trouble adding a clickListener
to the the buttons.
Here is the code for recycleView
adapter
class MyCategoryRecyclerViewAdapter(
private val values: ArrayList<String>
) : RecyclerView.Adapter<MyCategoryRecyclerViewAdapter.MyCategoryRecyclerViewHolder>() {
inner class MyCategoryRecyclerViewHolder :
RecyclerView.ViewHolder(binding.root) {
fun bind(category: String) {
binding.category = category
binding.btnCategoryName.setOnClickListener {
Log.d("Debug On", "Button clicked ${binding.btnCategoryName.text}")
}
}
}
private lateinit var binding: CategoryItemBinding
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyCategoryRecyclerViewHolder {
binding = CategoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyCategoryRecyclerViewHolder()
}
override fun onBindViewHolder(holder: MyCategoryRecyclerViewHolder, position: Int) {
holder.bind(values[position])
}
fun updateData(values: ArrayList<String>) {
this.values.clear()
this.values.addAll(values)
}
override fun getItemCount() = values.size
}
My ViewModel
for the categories fragment.
enum class FakeApiStatus { LOADING, ERROR, DONE }
class CategoryViewModel : ViewModel() {
// The internal MutableLiveData that stores the status of the most recent request
private val _status = MutableLiveData<FakeApiStatus>()
val status: LiveData<FakeApiStatus> = _status
val categories: ArrayList<String> = ArrayList()
/**
* Call getCategories() on init so we can display status immediately.
*/
init {
Log.d("Debug", "view model created")
getCategories()
}
private fun getCategories() {
viewModelScope.launch(Dispatchers.IO) {
_status.postValue(FakeApiStatus.LOADING)
try {
categories.clear()
categories.addAll(FakeApi.retrofitService.getCategories())
_status.postValue(FakeApiStatus.DONE)
} catch (e: Exception) {
_status.postValue(FakeApiStatus.ERROR)
}
}
}
}
Code for the fragment.
class CategoryFragment : Fragment() {
private lateinit var binding: FragmentCategoryBinding
private val adapter = MyCategoryRecyclerViewAdapter(ArrayList())
private val viewModel: CategoryViewModel by lazy {
ViewModelProvider(this)[CategoryViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCategoryBinding.inflate(inflater)
binding.rvCategoryList.adapter = adapter
viewModel.status.observe(viewLifecycleOwner) {
when (it) {
FakeApiStatus.DONE -> binding.animationRotation.visibility = View.GONE
FakeApiStatus.ERROR -> Toast.makeText(
context,
"No Internet!",
Toast.LENGTH_LONG
).show()
else -> {}
}
observeCategories()
}
return binding.root
}
private fun observeCategories(){
adapter.updateData(viewModel.categories)
adapter.notifyDataSetChanged()
}
}
This is the layout for my item
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="category"
type="String" />
</data>
<Button
android:id="@ id/btn_category_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{category.toUpperCase()}"
tools:text="Test"
android:onClick="TODO"
android:layout_margin="@dimen/cardview_default_elevation"
android:textAppearance="?attr/textAppearanceListItem" />
</layout>
And the layout for Fragment(RecycleView)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@ id/rv_category_list"
android:name="com.example.task.categories.CategoryFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".categories.CategoryFragment"
tools:listitem="@layout/category_item" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@ id/animation_rotation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
CodePudding user response:
You can pass a call back to the adapter class. In your fragment / activity, below the class create a new class to handle the click
/**
* Click listener for YourDomainObject. By giving the block a name it helps a reader understand what it does.
*/
class YourCustomClickClick(val block:(YourDomainObject)-> Unit){
fun onClick(yourDomainObject:YourDomainObject)= block(yourDomainObject)
}
When you initialize the viewModelAdapter in your fragment/activity you can also return the call back
viewModelAdapter = YourAdapter(YourCustomClick{item->
var value = item
Log.i(logTag,"The item that was clicked: ${value}")
//do any other operations you would like on click here
return@YourCustomClickCallback
}
Your adapter class:
class MyCategoryRecyclerViewAdapter(
private val values: ArrayList<String>
)
change to: pass a callback
class MyCategoryRecyclerViewAdapter(
private val values: ArrayList<String>,
val callback:YourCustomClick // the call back created in your fragment/activity
)
in your view holder you can get the positions of the click like so
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
holder.viewDataBinding.also {
it.item = item[position] // here we can tell what item is clicked based on the position in the list
it.YourCustomClickCallback= callback // assign the call back for each item in the list
}
}
finally, in your view you can gain access to the call back and domain object by creating data variables. inside your layout tags, add data tags. Inside the data tags add a variable with a name and path to the object. (if you have a domain object created yet).
<layout>
<data>
<variable
name="item"
type="com.example.yourproject.domain.models.Item" />
<variable
name="yourCustomCallback"
type="com..example.yourproject.main.ui.fragments.YourCustomClick" />
</data>
</layout>
Tip, this can be done in the xml layout that holds the cards or views to your recycler view. The view can be assigned from the view holder
class YourViewHolder(val viewDataBinding: CardViewYourItemBinding):
RecyclerView.ViewHolder(viewDataBinding.root){
companion object{
@LayoutRes
val LAYOUT = R.layout.card_view_your_recycler_item
}
}
You can assign the call back in your xml with data binding
<ImageView
android:clickable="true"
android:onClick="@{()->yourCustomCallback.onClick(item)}"
<!-- item is passed to your custom on click -->
By domain object I meant domain model. It is a simple class that you can instance to hold some data.
Here is an example of a simple data class for a user.
data class User(
val userID: String,
val fireBaseId:String,
val userName: String,
val firstName: String,
val lastName: String,
val score: Int,
val age: Int,
val emailAddress: String,
val photoUrl: String,
val sex: Int,
val emailVerified:Boolean)