ภาพรวม
RecyclerView.Adapter ต้องแจ้งให้ RecyclerView ทราบเมื่อข้อมูลเปลี่ยนแปลง ผ่าน Method notify ต่างๆ การใช้ notify ที่ถูกต้องสำคัญมากต่อ Performance และ Animation
Notify Methods (เรียงตามประสิทธิภาพ)
| Method | ใช้เมื่อ | Animation | Performance |
|---|---|---|---|
notifyItemInserted(pos) | เพิ่ม 1 Item | มี (Default) | ดีที่สุด |
notifyItemRemoved(pos) | ลบ 1 Item | มี | ดีมาก |
notifyItemChanged(pos) | อัปเดต 1 Item | มี | ดีมาก |
notifyItemRangeInserted(start, count) | เพิ่มหลาย Item ติดกัน | มี | ดี |
notifyItemRangeRemoved(start, count) | ลบหลาย Item ติดกัน | มี | ดี |
notifyItemRangeChanged(start, count) | อัปเดตช่วง Item | มี | ดี |
notifyItemMoved(from, to) | ย้าย Item | มี | ดี |
notifyDataSetChanged() | เปลี่ยนแปลงมาก/ซับซ้อน/ไม่รู้ตำแหน่ง | ไม่มี (Full Rebinding) | ต่ำสุด |
ตัวอย่างครบถ้วน: Student Records App (Add/Remove/Update)
1. Model (Student.java)
package com.example.myapplication;
public class Student {
public final String id; // Unique ID สำหรับ DiffUtil/Stable ID
public String name;
public int age;
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
2. Adapter (StudentAdapter.java)
package com.example.myapplication; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; import java.util.Random; public class StudentAdapter extends RecyclerView.Adapter{ private final List students = new ArrayList<>(); private final Random random = new Random(); private final OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(Student student, int position); void onItemLongClick(Student student, int position); } public StudentAdapter(OnItemClickListener listener) { this.listener = listener; setHasStableIds(true); // ใช้ Stable ID เพื่อ Animation ดีกว่า } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_student, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Student student = students.get(position); holder.bind(student); } @Override public int getItemCount() { return students.size(); } @Override public long getItemId(int position) { // Stable ID: ใช้ hash ของ ID string return students.get(position).id.hashCode(); } // ===== Data Manipulation Methods ===== public void addStudent(Student student) { students.add(student); notifyItemInserted(students.size() - 1); } public void addStudentAt(int position, Student student) { students.add(position, student); notifyItemInserted(position); } public void removeStudent(int position) { if (position >= 0 && position < students.size()) { students.remove(position); notifyItemRemoved(position); } } public void updateStudent(int position, Student newStudent) { if (position >= 0 && position < students.size()) { students.set(position, newStudent); notifyItemChanged(position); } } public void updateStudentName(int position, String newName) { if (position >= 0 && position < students.size()) { students.get(position).name = newName; notifyItemChanged(position); } } public void setStudents(List newList) { students.clear(); students.addAll(newList); notifyDataSetChanged(); // เปลี่ยนทั้งหมด } public Student getStudent(int position) { return students.get(position); } // ===== ViewHolder ===== static class ViewHolder extends RecyclerView.ViewHolder { TextView tvName, tvAge, tvId; View itemView; ViewHolder(View view) { super(view); itemView = view; tvName = view.findViewById(R.id.tvName); tvAge = view.findViewById(R.id.tvAge); tvId = view.findViewById(R.id.tvId); } void bind(Student student) { tvName.setText(student.name); tvAge.setText(student.age + " ปี"); tvId.setText("ID: " + student.id); // Random Background Color int color = Color.argb(255, random.nextInt(200), random.nextInt(200), random.nextInt(200)); itemView.setBackgroundColor(color); } } }
3. Item Layout (res/layout/item_student.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:card_view="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="12dp"
card_view:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#212121" />
<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textColor="#757575" />
<TextView
android:id="@+id/tvId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp"
android:textColor="#9E9E9E" />
</LinearLayout>
</androidx.cardview.widget.CardView>
4. MainActivity.java
package com.example.myapplication;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.UUID;
public class MainActivity extends AppCompatActivity
implements StudentAdapter.OnItemClickListener {
private RecyclerView recyclerView;
private StudentAdapter adapter;
private EditText etName, etAge;
private Button btnAdd, btnRemove, btnUpdate;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
etName = findViewById(R.id.etName);
etAge = findViewById(R.id.etAge);
btnAdd = findViewById(R.id.btnAdd);
btnRemove = findViewById(R.id.btnRemove);
btnUpdate = findViewById(R.id.btnUpdate);
adapter = new StudentAdapter(this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
// Initial Data
adapter.addStudent(new Student(UUID.randomUUID().toString(), "สมชาย ใจดี", 20));
adapter.addStudent(new Student(UUID.randomUUID().toString(), "สมหญิง รักเรียน", 21));
adapter.addStudent(new Student(UUID.randomUUID().toString(), "สมศักดิ์ เก่งมาก", 19));
btnAdd.setOnClickListener(v -> {
String name = etName.getText().toString().trim();
String ageStr = etAge.getText().toString().trim();
if (name.isEmpty() || ageStr.isEmpty()) {
Toast.makeText(this, "กรอกชื่อและอายุให้ครบ", Toast.LENGTH_SHORT).show();
return;
}
adapter.addStudent(new Student(UUID.randomUUID().toString(), name, Integer.parseInt(ageStr)));
etName.setText("");
etAge.setText("");
recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
});
btnRemove.setOnClickListener(v -> {
if (adapter.getItemCount() > 0) {
adapter.removeStudent(adapter.getItemCount() - 1); // ลบตัวสุดท้าย
} else {
Toast.makeText(this, "ไม่มีข้อมูลให้ลบ", Toast.LENGTH_SHORT).show();
}
});
btnUpdate.setOnClickListener(v -> {
String name = etName.getText().toString().trim();
if (name.isEmpty() || adapter.getItemCount() == 0) return;
adapter.updateStudentName(0, name); // อัปเดตตัวแรก
etName.setText("");
});
}
@Override
public void onItemClick(Student student, int position) {
Toast.makeText(this, "คลิก: " + student.name, Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(Student student, int position) {
adapter.removeStudent(position);
Toast.makeText(this, "ลบ: " + student.name, Toast.LENGTH_SHORT).show();
}
}
5. Activity Layout (res/layout/activity_main.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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/etName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="ชื่อ" />
<EditText
android:id="@+id/etAge"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:hint="อายุ"
android:inputType="number" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<Button
android:id="@+id/btnAdd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="เพิ่ม" />
<Button
android:id="@+id/btnRemove"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="ลบสุดท้าย" />
<Button
android:id="@+id/btnUpdate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="แก้ไขตัวแรก" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
DiffUtil (Advanced - สำหรับ List ใหม่ทั้งชุด)
แทน notifyDataSetChanged() ใช้ DiffUtil คำนวณ Diff แล้ว Dispatch Update Events อัตโนมัติ:
// ใน Adapter public void setStudentsDiffUtil(ListnewList) { DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new StudentDiffCallback(this.students, newList), true); this.students.clear(); this.students.addAll(newList); diffResult.dispatchUpdatesTo(this); } // Callback Class static class StudentDiffCallback extends DiffUtil.Callback { private final List oldList, newList; StudentDiffCallback(List oldList, List newList) { this.oldList = oldList; this.newList = newList; } @Override public int getOldListSize() { return oldList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldPos, int newPos) { return oldList.get(oldPos).id.equals(newList.get(newPos).id); } @Override public boolean areContentsTheSame(int oldPos, int newPos) { Student o = oldList.get(oldPos), n = newList.get(newPos); return o.name.equals(n.name) && o.age == n.age; } }
ListAdapter (Kotlin/Java 8+ - Built-in DiffUtil)
// ใช้ ListAdapter แทน RecyclerView.Adapter public class StudentAdapter extends ListAdapter{ protected StudentAdapter() { super(DIFF_CALLBACK); } private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback () { @Override public boolean areItemsTheSame(@NonNull Student o, @NonNull Student n) { return o.id.equals(n.id); } @Override public boolean areContentsTheSame(@NonNull Student o, @NonNull Student n) { return o.name.equals(n.name) && o.age == n.age; } }; // ... ViewHolder, onCreateViewHolder, onBindViewHolder เหมือนเดิม // ใช้ submitList(newList) แทน setStudents + notify }
สรุป
- ใช้ notifyItemInserted/Removed/Changed/Moved สำหรับการเปลี่ยนแปลงจุดเดียว/ช่วง → **มี Animation, Performance ดี
- หลีกเลี่ยง
notifyDataSetChanged()ถ้าเป็นไปได้ (Full Rebind, ไม่มี Animation) - ใช้ Stable IDs (getItemId) + DiffUtil/ListAdapter สำหรับ List Replacement
- Model ควรมี Unique ID (UUID, Server ID) สำหรับ DiffUtil/Stable ID
- Thread Safety: เรียก notify บน Main Thread เท่านั้น