ภาพรวม
บทความนี้สาธิตวิธีอัปเดต ListView แบบไดนามิก (เพิ่ม/ลบ/แก้ไข Item แบบ Real-time) ใน Android โดยใช้ ArrayAdapter และ notifyDataSetChanged() พร้อมแนวทางปัจจุบันแนะนำให้ย้ายไป RecyclerView + DiffUtil/ListAdapter เพื่อ Performance และ Animation ดีกว่า
ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่
สร้างโปรเจกต์ Android ใหม่ใน Android Studio
ขั้นตอนที่ 2: Layout หลัก (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="พิมพ์ข้อความเพื่อเพิ่ม..."
android:inputType="text" />
<LinearLayout
android:id="@+id/buttonGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/etInput"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:weightSum="3">
<Button
android:id="@+id/btnAdd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="➕ Add" />
<Button
android:id="@+id/btnInsert"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="➡ Insert Top" />
<Button
android:id="@+id/btnClear"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🗑 Clear All" />
</LinearLayout>
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/buttonGroup"
android:layout_marginTop="8dp"
android:divider="@android:color/darker_gray"
android:dividerHeight="1dp" />
</RelativeLayout>
ขั้นตอนที่ 3: MainActivity.java - Dynamic ListView Operations
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private EditText etInput;
private Button btnAdd, btnInsert, btnClear;
private ListView listView;
// Data Source - ต้องเป็น ArrayList เพื่อรองรับ Add/Remove
private final List items = new ArrayList<>();
private ArrayAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etInput = findViewById(R.id.etInput);
btnAdd = findViewById(R.id.btnAdd);
btnInsert = findViewById(R.id.btnInsert);
btnClear = findViewById(R.id.btnClear);
listView = findViewById(R.id.listView);
// เริ่มต้นด้วยข้อมูลตัวอย่าง
items.add("Apple");
items.add("Banana");
items.add("Cherry");
// Adapter - ใช้ Layout มาตรฐาน android.R.layout.simple_list_item_1
adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
items
);
listView.setAdapter(adapter);
// ===== Event Handlers =====
// เพิ่มท้ายรายการ
btnAdd.setOnClickListener(v -> {
String text = etInput.getText().toString().trim();
if (!TextUtils.isEmpty(text)) {
items.add(text); // เพิ่มท้าย
adapter.notifyDataSetChanged(); // แจ้ง Adapter ว่าข้อมูลเปลี่ยน
etInput.setText("");
scrollToBottom();
} else {
Toast.makeText(this, "กรุณากรอกข้อความ", Toast.LENGTH_SHORT).show();
}
});
// เพิ่มหน้ารายการ (Index 0)
btnInsert.setOnClickListener(v -> {
String text = etInput.getText().toString().trim();
if (!TextUtils.isEmpty(text)) {
items.add(0, text); // Insert ที่ Index 0
adapter.notifyDataSetChanged();
etInput.setText("");
listView.smoothScrollToPosition(0);
} else {
Toast.makeText(this, "กรุณากรอกข้อความ", Toast.LENGTH_SHORT).show();
}
});
// ลบทั้งหมด
btnClear.setOnClickListener(v -> {
if (!items.isEmpty()) {
items.clear();
adapter.notifyDataSetChanged();
Toast.makeText(this, "ล้างรายการทั้งหมด", Toast.LENGTH_SHORT).show();
}
});
// Long Click เพื่อลบ Item เดี่ยว
listView.setOnItemLongClickListener((parent, view, position, id) -> {
String removed = items.remove(position);
adapter.notifyDataSetChanged();
Toast.makeText(this, "ลบ: " + removed, Toast.LENGTH_SHORT).show();
return true;
});
// Click ปกติ
listView.setOnItemClickListener((parent, view, position, id) -> {
String item = items.get(position);
Toast.makeText(this, "Item " + position + ": " + item, Toast.LENGTH_SHORT).show();
});
}
private void scrollToBottom() {
listView.post(() -> listView.smoothScrollToPosition(items.size() - 1));
}
}
ขั้นตอนที่ 4: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<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>
Notify Methods เปรียบเทียบ
| Method | ใช้เมื่อ | Animation | Performance |
|---|---|---|---|
notifyDataSetChanged() | ข้อมูลเปลี่ยนมาก/ไม่แน่ใจ Index | ไม่มี (Reload ทั้งหมด) | ช้า (Rebind ทุก View) |
notifyItemInserted(pos) | เพิ่ม Item ที่ Index เฉพาะ | มี (Default Animator) | เร็ว |
notifyItemRemoved(pos) | ลบ Item ที่ Index เฉพาะ | มี | เร็ว |
notifyItemChanged(pos) | แก้ไข Item ที่ Index เฉพาะ | มี | เร็ว |
notifyItemRangeInserted(start, count) | เพิ่มหลาย Item ติดกัน | มี | เร็ว |
notifyItemMoved(from, to) | ย้าย Item | มี | เร็ว |
ปรับปรุง Performance: ใช้ notifyItemX แทน notifyDataSetChanged
// แทนที่ items.add(text); adapter.notifyDataSetChanged(); // ด้วย: int position = items.size(); items.add(text); adapter.notifyItemInserted(position); // แทนที่ items.add(0, text); adapter.notifyDataSetChanged(); // ด้วย: items.add(0, text); adapter.notifyItemInserted(0); // แทนที่ items.remove(position); adapter.notifyDataSetChanged(); // ด้วย: items.remove(position); adapter.notifyItemRemoved(position); adapter.notifyItemRangeChanged(position, items.size() - position); // Update positions หลังลบ
แนะนำ: ย้ายไป RecyclerView + ListAdapter + DiffUtil (Modern)
build.gradle
dependencies {
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.recyclerview:recyclerview-selection:1.2.1"
}
Model Class
public class Item {
private final String id; // Unique ID สำคัญสำหรับ DiffUtil
private String text;
public Item(String id, String text) {
this.id = id;
this.text = text;
}
// Getters, Setters, equals/hashCode based on id
}
ListAdapter with DiffUtil
public class ItemAdapter extends ListAdapter<Item, ItemAdapter.ViewHolder> {
public ItemAdapter() {
super(DIFF_CALLBACK);
}
private static final DiffUtil.ItemCallback<Item> DIFF_CALLBACK = new DiffUtil.ItemCallback<Item>() {
@Override
public boolean areItemsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
return oldItem.getId().equals(newItem.getId()); // เปรียบเทียบ ID
}
@Override
public boolean areContentsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
return oldItem.getText().equals(newItem.getText()); // เปรียบเทียบเนื้อหา
}
};
// ... onCreateViewHolder, onBindViewHolder ...
// การใช้งาน: adapter.submitList(newList) -> DiffUtil คำนวณ Diff อัตโนมัติ + Animation!
}
สรุป Best Practices
- Data Source: ใช้
ArrayListไม่ใช่ Array[] (Fixed Size) - Notify Granular: ใช้
notifyItemInserted/Removed/ChangedแทนnotifyDataSetChanged() - Unique ID: Model ควรมี ID ไม่ซ้ำ สำหรับ DiffUtil/RecyclerView
- Background Thread: การ Filter/Sort ข้อมูลมาก ควรทำใน Background แล้ว submitList
- RecyclerView: ใช้แทน ListView สำหรับโปรเจกต์ใหม่ (Performance, Animation, Flexibility)
- ListAdapter + DiffUtil: จัดการ Animation และ Diff อัตโนมัติ ไม่ต้อง Notify เอง