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

Cơ chế tái sử dụng (Recycling) của ListView/RecyclerView trên Android

Giới thiệu

Ví dụ này minh họa cơ chế View Recycling (tái sử dụng view) của RecyclerView (thế hệ mới của ListView) thông qua một danh sách thành phố với CardView. RecyclerView chỉ tạo số lượng ViewHolder vừa đủ lấp đầy màn hình + một chút, sau đó tái sử dụng khi cuộn.

Các bước thực hiện

Bước 1: Tạo dự án mới

Mở Android Studio, chọn File → New Project. Thêm dependency RecyclerView và CardView nếu chưa có:

dependencies {
    implementation "androidx.recyclerview:recyclerview:1.3.2"
    implementation "androidx.cardview:cardview:1.0.0"
}

Bước 2: Giao diện chính (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/card_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />

</LinearLayout>

Bước 3: Layout item CardView (res/layout/card_row.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/card"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="5dp"
    android:layout_marginLeft="5dp"
    android:layout_marginRight="5dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_country"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:textSize="18sp"
        android:textStyle="bold" />

</androidx.cardview.widget.CardView>

Bước 4: Adapter (src/DataAdapter.java)

package com.app.sample;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;

public class DataAdapter extends RecyclerView.Adapter<DataAdapter.ViewHolder> {
    private ArrayList<String> cities;

    public DataAdapter(ArrayList<String> cities) {
        this.cities = cities;
    }

    @Override
    public DataAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.card_row, viewGroup, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(DataAdapter.ViewHolder viewHolder, int i) {
        viewHolder.tv_country.setText(cities.get(i));
    }

    @Override
    public int getItemCount() {
        return cities.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tv_country;

        public ViewHolder(View view) {
            super(view);
            tv_country = view.findViewById(R.id.tv_country);
        }
    }
}

Bước 5: MainActivity (src/MainActivity.java)

package com.app.sample;

import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    private ArrayList<String> cities;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
    }

    private void initViews() {
        RecyclerView recyclerView = findViewById(R.id.card_recycler_view);
        recyclerView.setHasFixedSize(true);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(layoutManager);

        cities = new ArrayList<>();
        cities.add("Mumbai");
        cities.add("Chennai");
        cities.add("Kochin");
        cities.add("Bangalore");
        cities.add("Hyderabad");
        cities.add("Amaravati");
        cities.add("Goa");
        cities.add("Trivandram");
        cities.add("Coimbatore");
        cities.add("Vellore");
        cities.add("Salem");
        cities.add("Bhubaneswar");
        cities.add(""); // Mục trống test
        cities.add("Cuttack");
        cities.add("Mysore");

        RecyclerView.Adapter adapter = new DataAdapter(cities);
        recyclerView.setAdapter(adapter);

        // Xử lý click item
        recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            GestureDetector gestureDetector = new GestureDetector(getApplicationContext(),
                    new GestureDetector.SimpleOnGestureListener() {
                        @Override
                        public boolean onSingleTapUp(MotionEvent e) { return true; }
                    });

            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                View child = rv.findChildViewUnder(e.getX(), e.getY());
                if (child != null && gestureDetector.onTouchEvent(e)) {
                    int position = rv.getChildAdapterPosition(child);
                    if (position != RecyclerView.NO_POSITION) {
                        Toast.makeText(getApplicationContext(), cities.get(position), Toast.LENGTH_SHORT).show();
                    }
                }
                return false;
            }

            @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
            @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
        });
    }
}

Bước 6: Manifest (Manifests/AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.app.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>

Chạy ứng dụng

Kết nối thiết bị, nhấn Run. Cuộn danh sách, nhấn vào item để thấy Toast hiển thị tên thành phố.

Cơ chế tái sử dụng (Recycling) của ListView/RecyclerView trên Android

Giải thích cơ chế Recycling

  1. onCreateViewHolder: Chỉ được gọi khi cần ViewHolder mới (chưa có trong pool). Số lần gọi ≈ số item hiển thị trên màn hình + 1-2.
  2. onBindViewHolder: Được gọi mỗi khi một item cuộn vào màn hình. Gán dữ liệu mới vào ViewHolder đã tái sử dụng.
  3. ViewHolder Pattern: Lưu trữ reference các View con (findViewById) để tránh gọi lại tốn kém.
  4. RecycledViewPool: RecyclerView nội bộ quản lý pool ViewHolder theo viewType.

So sánh ListView vs RecyclerView

Đặc điểmListViewRecyclerView
ViewHolderTùy chọn (getTag/setTag)Bắt buộc (bắt buộc implement)
LayoutManagerCố định (vertical list)Linh hoạt (Linear, Grid, Staggered, Custom)
Item AnimatorKhó tùy chỉnhMặc định có animation, dễ custom
Item DecorationKhông cóCó (divider, spacing, v.v.)
Hiệu năngTốtTốt hơn, linh hoạt hơn

Kết luận

RecyclerView là lựa chọn chuẩn hiện nay. Hiểu cơ chế recycling giúp viết Adapter đúng, tránh bug hiển thị sai dữ liệu khi cuộn nhanh (do ViewHolder tái sử dụng chưa kịp bind dữ liệu mới).