I am trying to show data in nested recyclerview like play store after fetching data from the server.I have successfully retrieved and parse title
of parent recyclerview but unable to show horizontal recyclerview which is a child layout.
I am getting error in CategortAdapter.kt
at the time of viewmodel instantiation it is showing red line under ViewModelProvider(context)
in below line:
val viewModel = ViewModelProvider(context).get(CatImagesViewModel::class.java)
Below is my JSON response:
{
"status": "200",
"message": "Success",
"result": [
{
"_id": "60f516fa846e059e2f19c50c",
"category": "Shirts",
"sku": [
{
"name": "Oxford shirt",
"brand": "John players",
"price": "25",
"color": "Blue",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/shi1.jpg?alt=media&token=64779194-e3b5-484f-a610-c9a20648b64c"
},
{
"name": "Buttoned down",
"brand": "Gap originals",
"price": "45",
"color": "Pink",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/shi2.jpg?alt=media&token=0b207b90-f1bc-4771-b877-809648e6bdc1"
},
{
"name": "Collared",
"brand": "Arrow",
"price": "30",
"color": "White",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/shi3.jpg?alt=media&token=2c1bb3f8-e739-4f09-acbc-aa11fed795e3"
},
{
"name": "Printed",
"brand": "John players",
"price": "30",
"color": "Olive green",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/shi4.jpg?alt=media&token=666f94bf-4769-44fe-a909-3c81ca9262f7"
},
{
"name": "Hoodie",
"brand": "Levis",
"price": "44",
"color": "Yellow",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/shi5.jpg?alt=media&token=65fccef4-a882-4278-b5df-f00eb2785bf1"
}
]
},
{
"_id": "60f51c37846e059e2f19c50f",
"category": "Shoes",
"sku": [
{
"name": "Sneakers",
"brand": "Puma",
"price": "35",
"color": "Black and white",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/sho1.jpg?alt=media&token=d078988d-9e85-4313-bb4a-c9d46e09f0b9"
},
{
"name": "Running shoe",
"brand": "Nike",
"price": "99",
"color": "Multicolor",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/sho2.jpg?alt=media&token=ed2e7387-3cf6-44df-9f7d-69843eb0bcdf"
},
{
"name": "Yezzy",
"brand": "Adidas",
"price": "349",
"color": "Gray",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/sho3.jpg?alt=media&token=2c37ef76-37bb-4bdd-b36c-dea32857291f"
},
{
"name": "Sneakers",
"brand": "Puma",
"price": "79",
"color": "Black",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/sho4.jpg?alt=media&token=4acd763e-8b93-47cd-ba45-92f34af4cf83"
},
{
"name": "Joyride running",
"brand": "Nike",
"price": "80",
"color": "White",
"img": "https://firebasestorage.googleapis.com/v0/b/koovs-1ff31.appspot.com/o/sho5.jpg?alt=media&token=e3780dcc-52cb-49d5-9791-e0a44870716c"
}
]
}
]
}
In the parent recycler view which is vertical I want to show title category
as shown in JSON response and sku
in horizontal recycler view.
In child recycler view I want to show images only which comes under sku
array.
Below is my code:
ApiService.kt
interface ApiService {
@GET("getProducts")
suspend fun getCategories(): Response<Product>
@GET("getProducts")
suspend fun getCatImg(): Response<Product>
}
Product.kt
data class Product(
val message: String,
val result: List<Result>,
val status: String
)
Result.kt
data class Result(
val _id: String,
val category: String,
val sku: List<Sku>
)
Sku.kt
data class Sku(
val brand: String,
val color: String,
val img: String,
val name: String,
val price: String
)
parent_row.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/catTitle"
android:textStyle="bold"
android:textSize="18sp"
android:textColor="#345"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/childRecycler"
android:layout_below="@+id/catTitle"
android:layout_marginTop="8dp"/>
</RelativeLayout>
child_row.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="200dp"
android:layout_height="200dp"
app:cardCornerRadius="3dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/img"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
In below adapter I am fetching category title and child recycler view as i have successfully retrieved titles but unable to parse recyclerview:
CategoryAdapter.kt
class CategoryAdapter(private val context: Context,private val categories:List<Result>): RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return
ViewHolder(ParentRowBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val model = categories[position]
holder.binding.catTitle.text = model.category
holder.binding.childRecycler.setHasFixedSize(true)
holder.binding.childRecycler.layoutManager = LinearLayoutManager(context)
val viewModel = ViewModelProvider(context).get(CatImagesViewModel::class.java)
}
override fun getItemCount(): Int {
return categories.size
}
class ViewHolder(val binding:ParentRowBinding): RecyclerView.ViewHolder(binding.root)
}
CatImagesViewModel.kt
class CatImagesViewModel: ViewModel() {
private var catImageList:MutableLiveData<List<List<Sku>>> = MutableLiveData()
fun getCatImg(): LiveData<List<List<Sku>>>{
viewModelScope.launch(Dispatchers.IO) {
val retrofit = RetrofitClient.getRetrofitClient().create(ApiService::class.java)
val response = retrofit.getCatImg()
if(response.isSuccessful){
val skus: MutableList<List<Sku>> = mutableListOf()
val cnt = response.body()!!.result.size
for(i in 0 until cnt){
skus.add(response.body()!!.result[i].sku)
}
catImageList.postValue(skus)
}
}
return catImageList
}
}
Below is the adapter for parsing images in child recycler view which is horizontal one:
CatImgAdapter.kt
class CatImgAdapter(private val context: Context,private val imgList:List<Sku>): RecyclerView.Adapter<CatImgAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ChildRowBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val model = imgList[position]
Glide.with(context).load(model.img).into(holder.binding.img)
}
override fun getItemCount(): Int {
return imgList.size
}
data class ViewHolder(val binding:ChildRowBinding): RecyclerView.ViewHolder(binding.root)
}
Below is the ViewModel
for fetching category title vor vertical recycler view:
CategoriesViewModel.kt
class CategoriesViewModel: ViewModel() {
private var categoryList: MutableLiveData<List<Result>> = MutableLiveData()
fun getAllCategory(): LiveData<List<Result>> {
viewModelScope.launch(Dispatchers.IO) {
val retrofit = RetrofitClient.getRetrofitClient().create(ApiService::class.java)
val response = retrofit.getCategories()
if (response.isSuccessful) {
categoryList.postValue(response.body()!!.result)
}
}
return categoryList
}
}
Someone let me know what I am doing wrong or the best way to achieve the desired layout.
You are not supposed to instantiate your ViewModel
inside the recyclerview adapter
. Instead, you need to pass the data to your recyclerview adapter
.
You should be observing the LiveData
in your View
(activity
or fragment
) and then pass this data (List<Result>
here) to your recyclerview adapter
. ViewModel
should be used only inside these Views
.
Let me know if you need more explanation about it, I'll try to explain in detail :)
EDIT
I also noticed that CatImagesViewModel
you have
if(response.isSuccessful){
val cnt = response.body()!!.result.size
for(i in 0 until cnt){
catImageList.postValue(response.body()!!.result[i].sku)
}
}
The postvalue
will set the current result
's sku
to the MutableLiveData
, this means you will have sku
(List<Sku>
) from only 1 category
since the previous one is discarded with every iteration.
What you would like to do instead is,
class CatImagesViewModel: ViewModel() {
private var catImageList:MutableLiveData<List<List<<Sku>>> = MutableLiveData()
fun getCatImg(): LiveData<List<List<Sku>>>{
viewModelScope.launch(Dispatchers.IO) {
val retrofit = RetrofitClient.getRetrofitClient().create(ApiService::class.java)
val response = retrofit.getCatImg()
if(response.isSuccessful){
val skus: MutableList<List<List<Sku>>> = mutableListOf()
val cnt = response.body()!!.result.size
for(i in 0 until cnt){
skus.add(response.body()!!.result[i].sku)
}
catImageList.postValue(skus)
}
}
return catImageList
}
}
FINAL UPDATE
Ok, this is the code you need to load the items in a nested recyclerview, if the names of these classes are different than yours, then just create a new project and add this code for your reference, I'm attaching a working screenshot of the app below as a headsup, hope it fixes everything for you now :)
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getResult()
val adapter = ParentAdapter(this)
recyclerview_parent.adapter = adapter
viewModel.data.observe(this, {
adapter.setData(it)
})
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
private val service = RetrofitHelper().retrofit.create(ApiService::class.java)
private val _data: MutableLiveData<List<Result>> = MutableLiveData()
val data: LiveData<List<Result>> get() = _data
fun getResult() {
viewModelScope.launch {
val res = service.getProducts()
_data.postValue(res.body()?.result)
}
}
}
ApiService.kt
interface ApiService {
@GET("getProducts")
suspend fun getProducts(): Response<Product>
}
ParentAdapter.kt
class ParentAdapter(
private val context: Context
): RecyclerView.Adapter<ParentAdapter.ParentViewHolder>() {
private var data: List<Result>? = null
inner class ParentViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
fun setData(data: List<Result>) {
this.data = data
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
return ParentViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.item_parent,parent,false)
)
}
override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
val item = data?.get(position)
if (item != null) {
holder.itemView.category_title.text = item.category
val adapter = ChildAdapter(context)
adapter.setData(item.sku)
holder.itemView.recyclerview_child.adapter = adapter
}
}
override fun getItemCount(): Int {
return data?.size ?: 0
}
}
ChildAdapter.kt
class ChildAdapter(
private val context: Context
) : RecyclerView.Adapter<ChildAdapter.ChildViewHolder>() {
private var data: List<Sku>? = null
inner class ChildViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
fun setData(data: List<Sku>) {
this.data = data
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChildViewHolder {
return ChildViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.item_child, parent, false)
)
}
override fun onBindViewHolder(holder: ChildViewHolder, position: Int) {
val item = data?.get(position)
if (item != null) {
holder.itemView.label.text = item.name
Glide.with(context)
.load(item.img)
.transition(DrawableTransitionOptions.withCrossFade())
.into(holder.itemView.image)
}
}
override fun getItemCount(): Int {
return data?.size ?: 0
}
}
RetrofitHelper.kt
class RetrofitHelper {
val retrofit = Retrofit.Builder()
.baseUrl("https://koovs18.herokuapp.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
item_parent.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/category_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Category Title"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:textSize="24sp"
android:textColor="@color/black"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_child"/>
</LinearLayout>
item_child.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image"
android:layout_width="150dp"
android:layout_height="160dp"
android:layout_margin="4dp"/>
<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Product Label"
android:gravity="center"
android:textSize="16sp"
android:textColor="@color/black"
android:padding="4dp"/>
</LinearLayout>
Dependencies you would need in build.gradle
def coroutines_version = "1.4.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
implementation "androidx.activity:activity-ktx:1.2.4"