บทนำ
ตัวอย่างนี้สาธิตการทำ 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