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

วิธีอัปเดต ListView แบบไดนามิก (Dynamic Update) ใน Android

ภาพรวม

บทความนี้สาธิตวิธีอัปเดต 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ใช้เมื่อAnimationPerformance
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 เอง