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

วิธีอัปเดต RecyclerView Adapter Data ใน Android

ภาพรวม

RecyclerView.Adapter ต้องแจ้งให้ RecyclerView ทราบเมื่อข้อมูลเปลี่ยนแปลง ผ่าน Method notify ต่างๆ การใช้ notify ที่ถูกต้องสำคัญมากต่อ Performance และ Animation

Notify Methods (เรียงตามประสิทธิภาพ)

Methodใช้เมื่อAnimationPerformance
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(List newList) {
    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 เท่านั้น