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 + Adapter | LinearLayout + addView |
|---|---|---|
| Hiệu năng (nhiều item) | Tuyệt vời (recycle view) | Kém (tạo nhiều View object) |
| Animation | Có sẵn (DefaultItemAnimator) | Phải tự implement |
| Độ phức tạp code | Cao hơn | Thấp |
| Phù hợp | Danh sách lớn, cần performance | Form ít field (< 10), đơn giản |
| Lấy dữ liệu tất cả field | Dễ (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
doOnTextChangedhoặcTextWatchercẩ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.