Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách thêm/xóa View động (Dynamic Views) trong Android

Giới thiệu

Thêm/xóa View tại runtime (dynamic views) dùng cho form động, danh sách nhập liệu biến đổi (số điện thoại, email, địa chỉ...). Cách hiện đại: dùng RecyclerView + Adapter (khuyến nghị) hoặc ViewGroup.addView()/removeView() (đơn giản, ít item).

Cách 1: RecyclerView + Adapter (Khuyến nghị - Best Practice)

Hiệu năng tốt, hỗ trợ animation, diffUtil, view recycling.

Dependencies

implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.recyclerview:recyclerview-selection:1.2.1") // Tùy chọn

Item Layout (res/layout/item_dynamic_field.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="horizontal"
    android:padding="12dp"
    android:gravity="center_vertical">

    <EditText
        android:id="@+id/etValue"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Nhập giá trị"
        android:inputType="text"
        android:padding="12dp"
        android:background="@drawable/edittext_rounded" />

    <Button
        android:id="@+id/btnDelete"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/ic_delete"
        android:contentDescription="Xóa" />
</LinearLayout>

Adapter (DynamicFieldAdapter.kt)

package com.example.dynamicviews

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.dynamicviews.databinding.ItemDynamicFieldBinding

data class DynamicField(
    val id: Long = System.currentTimeMillis(),
    var value: String = ""
)

class DynamicFieldAdapter(
    private val onDelete: (Long) -> Unit
) : ListAdapter(DiffCallback()) {

    class ViewHolder(private val binding: ItemDynamicFieldBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(field: DynamicField, onDelete: (Long) -> Unit) {
            binding.etValue.setText(field.value)
            binding.etValue.doOnTextChanged { text, _, _, _ ->
                // Cập nhật model khi text thay đổi
                // Lưu ý: cần cách tốt hơn để tránh rebind liên tục (xem bên dưới)
            }
            binding.btnDelete.setOnClickListener { onDelete(field.id) }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemDynamicFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position), onDelete)
    }

    class DiffCallback : DiffUtil.ItemCallback() {
        override fun areItemsTheSame(oldItem: DynamicField, newItem: DynamicField) = oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: DynamicField, newItem: DynamicField) = oldItem.value == newItem.value
    }
}

MainActivity.kt (RecyclerView)

package com.example.dynamicviews

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.dynamicviews.databinding.ActivityMainBinding
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val adapter = DynamicFieldAdapter { id ->
        // Xóa item bằng ID
        val currentList = adapter.currentList.toMutableList()
        currentList.removeAll { it.id == id }
        adapter.submitList(currentList)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.rvFields.layoutManager = LinearLayoutManager(this)
        binding.rvFields.adapter = adapter

        binding.btnAddField.setOnClickListener {
            val newField = DynamicField()
            val currentList = adapter.currentList.toMutableList()
            currentList.add(newField)
            adapter.submitList(currentList)
        }
    }
}

Cách 2: LinearLayout + addView/removeView (Đơn giản, ít item)

Phù hợp: ít item (< 10), không cần recycle, UI đơn giản.

Layout (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:id="@+id/parentLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/containerFields"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" />

    <Button
        android:id="@+id/btnAddField"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_gravity="center"
        android:text="Thêm trường" />
</LinearLayout>

Field Layout (res/layout/item_field.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="horizontal"
    android:padding="8dp"
    android:gravity="center_vertical">

    <EditText
        android:id="@+id/etValue"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Nhập giá trị"
        android:padding="12dp"
        android:background="@drawable/edittext_rounded" />

    <Button
        android:id="@+id/btnDelete"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/ic_delete"
        android:contentDescription="Xóa" />
</LinearLayout>

MainActivity.kt (addView/removeView)

package com.example.dynamicviews

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import com.example.dynamicviews.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnAddField.setOnClickListener { addField() }
        
        // Thêm 1 field mặc định
        addField()
    }

    private fun addField() {
        val fieldView = LayoutInflater.from(this).inflate(R.layout.item_field, binding.containerFields, false)
        
        val btnDelete = fieldView.findViewById(R.id.btnDelete)
        btnDelete.setOnClickListener {
            // Xóa view cha của button (LinearLayout item_field)
            (btnDelete.parent as? View)?.let { view ->
                binding.containerFields.removeView(view)
            }
        }
        
        binding.containerFields.addView(fieldView)
    }
}

So sánh 2 cách

Tiêu chíRecyclerView + AdapterLinearLayout + addView
Hiệu năng (nhiều item)Tuyệt vời (recycle view)Kém (tạo nhiều View object)
AnimationCó sẵn (DefaultItemAnimator)Phải tự implement
Độ phức tạp codeCao hơnThấp
Phù hợpDanh sách lớn, cần performanceForm ít field (< 10), đơn giản
Lấy dữ liệu tất cả fieldDễ (adapter.currentList)Duyệt containerFields.children

Lấy dữ liệu từ tất cả field (Cách 2)

private fun getAllFieldValues(): List {
    val values = mutableListOf()
    for (i in 0 until binding.containerFields.childCount) {
        val child = binding.containerFields.getChildAt(i)
        val etValue = child.findViewById(R.id.etValue)
        values.add(etValue.text.toString())
    }
    return values
}

Lưu ý quan trọng

  • RecyclerView là chuẩn cho dynamic list trong Android hiện đại.
  • Với addView: gán tag hoặc ID duy nhất cho từng field để dễ quản lý.
  • EditText trong dynamic view: dùng doOnTextChanged hoặc TextWatcher cẩn thận để không leak/loop.
  • ScrollView chứa LinearLayout dynamic: có thể lag nếu nhiều item → dùng NestedScrollView hoặc RecyclerView.
  • Save/restore state: RecyclerView tự handle, addView cần tự save/restore.