Nested Recyclerview

1.Recyclerview trong recyclerview.

  • Chắc hẳn đã có rất nhiều lần bạn thấy một giao diện mà trong đó có một danh sách các item mà bản thân các item đó lại là một danh sách khác, nghe có vẻ mơ hồ phải không ạ, chắc bạn sẽ biết giao diện này chứ:
    Chúng ta nhận thấy rằng danh sách các hạng mục ứng dụng như recommend hay top rated bản thân nó không đơn thuần chỉ là một item mà bản thân nó lại là một danh sách khác với các item của riêng chúng, làm sao để có thể làm được như vậy? Đừng lo hôm nay mình sẽ giới thiệu đến các bạn một cách đơn giản để xây dưng một recyclerview trong recyclerview, ví dụ trong bài mình làm đơn giản với data fix sẵn, tuy nhiên các bạn có thể thay đổi, làm thêm loadmore hay swipe to refresh, data online v.v.., tuy nhiên trong giới hạn bài này mình chỉ xin giới thiệu một ví dụ ở mức độ cơ bản để các bạn có thể dễ dàng hiểu được.

2. Xây dựng.

  • Trình bày như vậy là đủ, giờ ta sẽ bắt đầu vào việc xây dựng, trong ví dụ này mình sẽ gọi recyclerview bao ngoài là recyclerview cha còn recyclerview bên trong nó là recyclerview con.
  • Đầu tiên là xây dựng hai đối tương, ở đây mình làm đơn giản gồm hai đối tượng là Parent và Child, đối tượng Parent sẽ có một thuộc tính là đối tượng con Child, hai đối tượng này sẽ chính là data cho hai recyclerview của chúng ta
  • Đối tượng con (Child): Gồm hai thuộc tính là name và cost.
package com.example.framgianguyenthanhtungh.expandablerecycler.model

data class Child(val name: String, val cost: Int)
  • Đối tượng cha (Parent): Gồm một thuộc tính name và một thuộc tính children.
package com.example.framgianguyenthanhtungh.expandablerecycler.model

import com.example.framgianguyenthanhtungh.expandablerecycler.model.Child

data class Parent(val name: String, val children: List<Child>)

Các bạn có thể thấy, đối tượng cha có một list đối tượng con, list này chính là list trong recyclerview con trong recyclerview cha.

  • Sau khi xây dựng xong các đối tượng giờ chúng ta sẽ xây dựng các layout cần thiết.
  • Đầu tiên là layout cho các item Child:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/child_name"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/child_cost"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/child_name" />

</android.support.constraint.ConstraintLayout>

Ở layout này mình chỉ đơn giản hiển thị lên hai thuộc tính của đối tượng Child thông qua hai textview.

  • Layout cho các item Parent:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/parent_name"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:background="@color/colorPrimary"
        android:gravity="start|center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/list_child"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_56"
        android:background="@color/colorAccent"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/parent_name" />

</android.support.constraint.ConstraintLayout>

Layout này mình có một textview để hiển thị tên của đối tượng Parent và một recyclerview khác, đến đây chắc các bạn cũng đã hiểu cách mình làm rồi đúng không ạ, với mỗi item Parent trong recyclerview sẽ có một recyclerview khác mình vừa định nghĩa ở trên, và recyclerview này sẽ nhận data chính là listChild trong đối tượng Parent mà mình đã xây dựng ở trên để hiển thị.

  • Cuối cùng là layout cho main_activity nơi mà recyclerview cha được xây dựng.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_parent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
  • Vậy là đã xong phần giao diện, đối với một recyclerview ngoài giao diện ra ta còn phải xây dựng một adapter cho nó và ở đây cũng không ngoại lệ, mình sẽ viết hai adapter cho hai recyclerview
  • Đầu tiên là adapter cho recyclerview Child:
package com.example.framgianguyenthanhtungh.expandablerecycler.adapter

import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.example.framgianguyenthanhtungh.expandablerecycler.R
import com.example.framgianguyenthanhtungh.expandablerecycler.model.Child

class ChildAdapter(private val children: List<Child>) : RecyclerView.Adapter<ChildAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_child, parent, false)
        return ViewHolder(v)
    }

    override fun getItemCount(): Int {
        return children.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val child = children[position]
        holder.name.text = child.name
        holder.cost.text = child.cost.toString()
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val name: TextView = itemView.findViewById(R.id.child_name)
        val cost: TextView = itemView.findViewById(R.id.child_cost)
    }
}.
  • Với ChildAdapter thì mọi thứ hoàn toàn giống một adapter bình thường, bây giờ chúng ta sẽ xây dưng adapter cho recyclerview Parent:
package com.example.framgianguyenthanhtungh.expandablerecycler.adapter

import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.example.framgianguyenthanhtungh.expandablerecycler.R
import com.example.framgianguyenthanhtungh.expandablerecycler.model.Parent

class ParentAdapter(private val parents: List<Parent>) : RecyclerView.Adapter<ParentAdapter.ViewHolder>() {

    private var viewPool = RecyclerView.RecycledViewPool()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.parent_item, parent, false)
        return ViewHolder(view)
    }

    override fun getItemCount(): Int {
        return parents.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val parent = parents[position]
        holder.textView.text = parent.name
        holder.recyclerView.apply {
            layoutManager = LinearLayoutManager(holder.recyclerView.context, LinearLayout.HORIZONTAL, false)
            adapter = ChildAdapter(parent.children)
            setRecycledViewPool(viewPool)
        }
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val recyclerView: RecyclerView = itemView.findViewById(R.id.list_child)
        val textView: TextView = itemView.findViewById(R.id.parent_name)
    }
}

Ở đây đã có sự khác biệt, trogn hàm bindViewHolder ngoài việc setText cho textview mình đồng thời setAdapter cho recyclerview bên trong layout Parent lúc nãy bằng ChildAdapter mà chúng ta đã xây dựng ở trên. Ở đây có một điểm mà các bạn cần hết sức lưu ý đó là setRecycledViewPool(viewPool) tại sao lại cần lấy viewpool và truyền cho các recyclerview con? Vấn đề ở đây là nếu các bạn quên truyền thông số này vào thì khi bạn vuốt các recyclerview con thì không có vấn đề gì xảy ra và mọi thứ hoạt động dường như là đúng đắn tuy nhiên khi bạn vuốt recyclerview theo chiều dọc cảu recyclerview cha thì các recyclerview con sẽ được inflate lại. Điều này xảy ra bởi vì các recyclerview con này bản thân nó có các viewpool của chính nó, và ở đây mình truyền viewpool vào cho các recyclerview con thì vấn đề sẽ được giải quyết vì khi này các recyclerview con sẽ có cùng một viewpool rồi. Các bạn hoàn toàn có thể bỏ phần này và chạy thử app để hiểu vấn đề hơn.

  • Sau khi tạo xong adapter thì chúng ta sẽ tạo dữ liệu cho recyclerview, ở đây mình làm đơn giản nên chỉ fake một chút data offline thôi, các bạn có thể xây dựng phần này tùy vào mong muốn của các bạn.
package com.example.framgianguyenthanhtungh.expandablerecycler.data

import com.example.framgianguyenthanhtungh.expandablerecycler.model.Child
import com.example.framgianguyenthanhtungh.expandablerecycler.model.Parent

fun createChildItem1(): List<Child> {
    return listOf(
        Child("1", 1),
        Child("1", 2),
        Child("1", 3),
        Child("1", 4),
        Child("1", 5)
    )
}

fun createChildItem2(): List<Child> {
    return listOf(
        Child("2", 1),
        Child("2", 2),
        Child("2", 3),
        Child("2", 4),
        Child("2", 5)
    )
}

fun createChildItem3(): List<Child> {
    return listOf(
        Child("3", 1),
        Child("3", 2),
        Child("3", 3),
        Child("3", 4),
        Child("3", 5)
    )
}

fun createChildItem4(): List<Child> {
    return listOf(
        Child("4", 1),
        Child("4", 2),
        Child("4", 3),
        Child("4", 4),
        Child("4", 5)
    )
}

fun createParent(): List<Parent> {
    return listOf(
        Parent(
            "1",
            createChildItem1()
        ),
        Parent(
            "2",
            createChildItem2()
        ),
        Parent(
            "3",
            createChildItem3()
        ),
        Parent(
            "4",
            createChildItem4()
        )
    )
}
  • Rồi, vậy là gần xong phần dữ liệu, giờ chỉ còn xây dựng nốt activity rồi chạy thôi.
package com.example.framgianguyenthanhtungh.expandablerecycler

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.widget.LinearLayout
import com.example.framgianguyenthanhtungh.expandablerecycler.adapter.ParentAdapter
import com.example.framgianguyenthanhtungh.expandablerecycler.data.createParent
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initRecycler()
    }

    private fun initRecycler(){
        recycler_parent.apply {
            layoutManager = LinearLayoutManager([email protected], LinearLayout.VERTICAL, false)
            adapter = ParentAdapter(createParent())
        }

    }
}

Ở đây các bạn khai báo setAdapter cho recyclerviwe Parent một cách bình thường. Giờ các bạn có thể chạy và xem kết quả, giao diện khá là xấu =)) vì mình chỉ xây dựng đơn giản để cho các bạn hiểu thôi. Mong rằng bài này giúp ích được cho các bạn nếu có sai sót gì các bạn có thể comment góp ý cho bài viết được hoàn thiện hơn.

3.Source code

https://github.com/nguyenthanhtunghframgia/ExpandableRecycler
Các bạn có thể xem source code của project của mình tại đây để tham khảo. Cảm ơn các bạn đã theo dõi.