Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีทำ Endless List (Infinite Scroll) ด้วย RecyclerView ใน Android Kotlin

บทนำ

ตัวอย่างนี้สาธิตการทำ Endless List (Infinite Scrolling) หรือการโหลดข้อมูลเพิ่มเติมเมื่อเลื่อนจอถึงด้านล่าง โดยใช้ RecyclerView และ ScrollListener

ขั้นตอนที่ 1: สร้างโปรเจกต์

สร้างโปรเจกต์ Android ใหม่ใน Android Studio

ขั้นตอนที่ 2: Dependencies (build.gradle)

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.cardview:cardview:1.0.0'
}

ขั้นตอนที่ 3: Layout หลัก (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
    xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:listitem="@layout/item_row" />

ขั้นตอนที่ 4: Item Layout (res/layout/item_row.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardElevation="2dp"
    app:cardUseCompatPadding="true">

    <TextView
        android:id="@+id/textViewItem"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textColor="@android:color/holo_blue_bright"
        android:textStyle="bold"
        android:textSize="16sp" />
</androidx.cardview.widget.CardView>

ขั้นตอนที่ 5: Loading Item Layout (res/layout/item_loading.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true" />
</LinearLayout>

ขั้นตอนที่ 6: RecyclerViewAdapter.kt

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.NonNull
import androidx.recyclerview.widget.RecyclerView

class RecyclerViewAdapter(private val itemList: List) : RecyclerView.Adapter() {

    companion object {
        const val VIEW_TYPE_ITEM = 0
        const val VIEW_TYPE_LOADING = 1
    }

    @NonNull
    override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            VIEW_TYPE_ITEM -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_row, parent, false)
                ItemViewHolder(view)
            }
            VIEW_TYPE_LOADING -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_loading, parent, false)
                LoadingViewHolder(view)
            }
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun onBindViewHolder(@NonNull holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ItemViewHolder -> {
                val item = itemList[position] as String
                holder.bind(item)
            }
            is LoadingViewHolder -> {
                holder.progressBar.isIndeterminate = true
            }
        }
    }

    override fun getItemCount(): Int = itemList.size

    override fun getItemViewType(position: Int): Int {
        return if (itemList[position] is String) VIEW_TYPE_ITEM else VIEW_TYPE_LOADING
    }

    inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val tvItem: TextView = itemView.findViewById(R.id.textViewItem)
        fun bind(text: String) {
            tvItem.text = text
        }
    }

    class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
    }
}

ขั้นตอนที่ 7: MainActivity.kt

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: RecyclerViewAdapter
    private val items = mutableListOf()
    private var isLoading = false
    private var currentPage = 0
    private val pageSize = 20

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        title = "Endless RecyclerView"

        recyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        
        adapter = RecyclerViewAdapter(items)
        recyclerView.adapter = adapter

        loadInitialData()
        setupScrollListener()
    }

    private fun loadInitialData() {
        // โหลดข้อมูลหน้าแรก
        val newItems = generateItems(0, pageSize)
        items.addAll(newItems)
        adapter.notifyDataSetChanged()
    }

    private fun setupScrollListener() {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                
                if (dy <= 0) return // เลื่อนขึ้น ไม่ต้องโหลดเพิ่ม
                
                val layoutManager = recyclerView.layoutManager as LinearLayoutManager
                val visibleItemCount = layoutManager.childCount
                val totalItemCount = layoutManager.itemCount
                val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()

                // โหลดเพิ่มเมื่อเหลือ 5 รายการถึงด้านล่าง
                if (!isLoading && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount - 5) {
                    loadMoreData()
                }
            }
        })
    }

    private fun loadMoreData() {
        isLoading = true
        
        // เพิ่ม Loading item
        items.add(LoadingItem())
        adapter.notifyItemInserted(items.size - 1)

        // จำลอง Network Delay
        Handler(Looper.getMainLooper()).postDelayed({
            // ลบ Loading item
            items.removeAt(items.size - 1)
            adapter.notifyItemRemoved(items.size)
            
            // เพิ่มข้อมูลใหม่
            val newItems = generateItems(currentPage + 1, pageSize)
            val startPosition = items.size
            items.addAll(newItems)
            adapter.notifyItemRangeInserted(startPosition, newItems.size)
            
            currentPage++
            isLoading = false
        }, 1500)
    }

    private fun generateItems(page: Int, count: Int): List {
        return (page * count until (page + 1) * count).map { "Item #${it + 1}" }
    }

    // Marker class สำหรับ Loading item
    private class LoadingItem
}

ขั้นตอนที่ 8: Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.example.endlesslist">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

แนวทางทันสมัย: Paging 3 Library

Google แนะนำให้ใช้ Paging 3 สำหรับ Infinite Scroll แบบ Production-ready:

// build.gradle
implementation 'androidx.paging:paging-runtime:3.2.1'
implementation 'androidx.paging:paging-rxjava3:3.2.1' // หรือ paging-compose

// PagingSource
class ItemPagingSource(private val api: ApiService) : PagingSource() {
    override suspend fun load(params: LoadParams): LoadResult {
        val page = params.key ?: 1
        return try {
            val response = api.getItems(page, 20)
            LoadResult.Page(
                data = response.items,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.items.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

// PagingDataAdapter
class ItemAdapter : PagingDataAdapter(ItemDiffCallback()) {
    // ...
}

// ใน Activity
val pager = Pager(PagingConfig(pageSize = 20)) { ItemPagingSource(api) }
lifecycleScope.launch {
    pagingDataAdapter.submitData(pager.flow)
}

จุดสำคัญของ Endless List

  • Threshold - เริ่มโหลดก่อนถึงล่างสุด (เช่น 5 items)
  • Loading State - แสดง ProgressBar ป้องกันโหลดซ้ำ
  • Error Handling - Retry mechanism เมื่อโหลดล้มเหลว
  • Empty State - จัดการกรณีไม่มีข้อมูล
  • Memory - พิจารณา Paging/Lazy loading สำหรับข้อมูลเยอะมาก

การปรับปรุงประสิทธิภาพ

  • ใช้ DiffUtil หรือ ListAdapter แทน notifyDataSetChanged()
  • ตั้งค่า recyclerView.setItemViewCacheSize(20)
  • ใช้ setHasFixedSize(true) ถ้าขนาด Item ไม่เปลี่ยน
  • หลีกเลี่ยง Layout ซ้อนซ้อนเกินไปใน Item View