Search code examples
androidkotlinandroid-recyclerviewviewmodel

Unable to retrieve data in nested recycler view like playstore


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.


Solution

  • 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 :)

    enter image description here

    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"