Tải dữ liệu động với Recycler View

Trong quá trình sử dụng app Android , chắc hẳn nhiều khi các bạn đã thấy các hoạt động như là có 1 danh sách 10 bài viết , kéo xuống đọc hết 10 bài thì nó lại ra tiếp 10 bài nữa. Như thế này này :

Điều này rất là dễ dàng giúp cho chúng ta có thể điều khiển được việc tải dữ liệu , tránh việc tải dư thừa dữ liệu. Chẳng hạn có 1000 bài viết thì mình cần lấy 10 bài mới nhất cho người dùng đọc thôi , còn người ta muốn đọc tiếp thì tải tiếp chẳng hạn...

Vậy trong bài hướng dẫn hôm nay , mình xin hướng dẫn các bạn kỹ thuật Dynamic load data trong Recycler View

1. Chuẩn bị

Ở đây , vì bài này sử dụng RecyclerView nên mình xin yêu cầu các bạn trước khi bắt tay vào làm ví dụ này thì nên biết rõ cách tạo 1 RecyclerView và hiển thị dữ liệu lên nó đã RecyclerView thì chúng ta cần cài thư viện đó đã Trong ví dụ này , mình sử dụng thêm cardview để tạo danh sách thẻ cho nó trông thật là bắt mắt hơn 1 tí

Đầu tiên , tạo 1 interface ILoadmore để xử lý việc tải thêm dữ liệu sau

public interface ILoadMore {
    void onLoadMore();
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="edmt.dev.androidrcldynamic.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Tạo 2 layout mới , item_layout dành cho việc hiển thị các item và item_loading dành cho hiển thị progress ring

item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardElevation="10dp"
    android:layout_margin="8dp"
    >

    <LinearLayout
        android:orientation="vertical"
        android:padding="10dp"
        android:background="?android:selectableItemBackground"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/txtName"
            android:text="Name"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:textStyle="bold"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/txtLength"
            android:text="Length"
            android:textColor="@android:color/black"
            android:textSize="14sp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

</android.support.v7.widget.CardView>

item_loading.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        />

</LinearLayout>

RecyclerView luôn cần có Recycler.Adapter , tạo 1 lớp MyAdapter.java

package edmt.dev.androidrcldynamic.Adapter;

import android.app.Activity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.util.List;

import edmt.dev.androidrcldynamic.Interface.ILoadMore;
import edmt.dev.androidrcldynamic.Model.Item;
import edmt.dev.androidrcldynamic.R;

/**
 * Created by reale on 10/10/2017.
 */

class LoadingViewHolder extends RecyclerView.ViewHolder
{

    public ProgressBar progressBar;

    public LoadingViewHolder(View itemView) {
        super(itemView);
        progressBar = (ProgressBar)itemView.findViewById(R.id.progressBar);
    }
}

class ItemViewHolder extends RecyclerView.ViewHolder{

    public TextView name,length;

    public ItemViewHolder(View itemView) {
        super(itemView);
        name = (TextView)itemView.findViewById(R.id.txtName);
        length = (TextView)itemView.findViewById(R.id.txtLength);
    }
}

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final int VIEW_TYPE_ITEM=0,VIEW_TYPE_LOADING=1;
    ILoadMore loadMore;
    boolean isLoading;
    Activity activity;
    List<Item> items;
    int visibleThreshold=5;
    int lastVisibleItem,totalItemCount;

    public MyAdapter(RecyclerView recyclerView,Activity activity, List<Item> items) {
        this.activity = activity;
        this.items = items;

        final LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager();
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                totalItemCount = linearLayoutManager.getItemCount();
                lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
                if(!isLoading && totalItemCount <= (lastVisibleItem+visibleThreshold))
                {
                    if(loadMore != null)
                        loadMore.onLoadMore();
                    isLoading = true;
                }

            }
        });
    }



    @Override
    public int getItemViewType(int position) {
        return items.get(position) == null ? VIEW_TYPE_LOADING:VIEW_TYPE_ITEM;
    }

    public void setLoadMore(ILoadMore loadMore) {
        this.loadMore = loadMore;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == VIEW_TYPE_ITEM)
        {
            View view = LayoutInflater.from(activity)
                    .inflate(R.layout.item_layout,parent,false);
            return new ItemViewHolder(view);
        }
        else if(viewType == VIEW_TYPE_LOADING)
        {
            View view = LayoutInflater.from(activity)
                    .inflate(R.layout.item_loading,parent,false);
            return new LoadingViewHolder(view);
        }
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        if(holder instanceof  ItemViewHolder)
        {
            Item item = items.get(position);
            ItemViewHolder viewHolder = (ItemViewHolder) holder;
            viewHolder.name.setText(items.get(position).getName());
            viewHolder.length.setText(String.valueOf(items.get(position).getLength()));
        }
        else if(holder instanceof LoadingViewHolder)
        {
            LoadingViewHolder loadingViewHolder = (LoadingViewHolder)holder;
            loadingViewHolder.progressBar.setIndeterminate(true);
        }

    }

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

    public void setLoaded() {
        isLoading = false;
    }
}

Ở đây chúng ta tạo ra 2 ViewHolder tương ứng với 2 layout

Layout Loading (sẽ hiển thị khi chúng ta tải dữ liệu mới)

class LoadingViewHolder extends RecyclerView.ViewHolder
{

    public ProgressBar progressBar;

    public LoadingViewHolder(View itemView) {
        super(itemView);
        progressBar = (ProgressBar)itemView.findViewById(R.id.progressBar);
    }
}

Layout Item (các item sẽ hiển thị theo thiết kế này)


class ItemViewHolder extends RecyclerView.ViewHolder{

    public TextView name,length;

    public ItemViewHolder(View itemView) {
        super(itemView);
        name = (TextView)itemView.findViewById(R.id.txtName);
        length = (TextView)itemView.findViewById(R.id.txtLength);
    }
}

Mấu chốt của tutorial nằm ở đây , ta sẽ xử lý việc tải thêm dữ liệu bằng cách set sự kiện onScrollListener cho RecyclerView ở hàm khởi tạo của Adapter

 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                totalItemCount = linearLayoutManager.getItemCount(); // Lấy tổng số lượng item đang có
                lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition(); // Lấy vị trí của item cuối cùng
                if(!isLoading && totalItemCount <= (lastVisibleItem+visibleThreshold)) // Nếu không phải trạng thái loading và tổng số lượng item bé hơn hoặc bằng vị trí item cuối + số lượng item tối đa hiển thị
                {
                    if(loadMore != null)
                        loadMore.onLoadMore(); // Gọi interface Loadmore
                    isLoading = true;
                }

            }
        });

Override hàm getItemViewType để xử lý kiểu của View

 @Override
    public int getItemViewType(int position) {
        return items.get(position) == null ? VIEW_TYPE_LOADING:VIEW_TYPE_ITEM; // So sánh nếu item được get tại vị trí này là null thì view đó là loading view , ngược lại là item
    }

Override hàm onCreateViewHolder để xử lý việc hiển thị dựa trên kiểu của View (chúng ta có thể dùng cái này để custom các layout item của RecyclerView nhưng đó là ở bài khác)

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == VIEW_TYPE_ITEM)
        {
            View view = LayoutInflater.from(activity)
                    .inflate(R.layout.item_layout,parent,false);
            return new ItemViewHolder(view);
        }
        else if(viewType == VIEW_TYPE_LOADING)
        {
            View view = LayoutInflater.from(activity)
                    .inflate(R.layout.item_loading,parent,false);
            return new LoadingViewHolder(view);
        }
        return null;
    }

Và ở hàm onBindViewHolder , chúng ta sẽ so sánh các thể hiện của View để xử lý nó

  if(holder instanceof  ItemViewHolder)
        {
            Item item = items.get(position);
            ItemViewHolder viewHolder = (ItemViewHolder) holder;
            viewHolder.name.setText(items.get(position).getName());
            viewHolder.length.setText(String.valueOf(items.get(position).getLength()));
        }
        else if(holder instanceof LoadingViewHolder)
        {
            LoadingViewHolder loadingViewHolder = (LoadingViewHolder)holder;
            loadingViewHolder.progressBar.setIndeterminate(true);
        }

Đừng quên tạo setter cho ILoadmore

    public void setLoadMore(ILoadMore loadMore) {
        this.loadMore = loadMore;
    }

Okay , giờ tạo 1 Model item mẫu theo dạng sau : Tạo lớp Item.java

public class Item {
    private String name;
    private int length;

    public Item(String name, int length) {
        this.name = name;
        this.length = length;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }
}

Các bạn có thể thấy , lớp Item này chỉ có 2 thuộc tính là name và length

Và bây giờ tiến hành code trong MainActivity thôi

public class MainActivity extends AppCompatActivity {

    List<Item> items = new ArrayList<>();
    MyAdapter adapter;



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

        //Random data
        random10Data();

        //Init View
        RecyclerView recycler = (RecyclerView)findViewById(R.id.recycler);

        recycler.setLayoutManager(new LinearLayoutManager(this));
        adapter = new MyAdapter(recycler,this,items);
        recycler.setAdapter(adapter);

        //Set Load more event
        adapter.setLoadMore(new ILoadMore() {
            @Override
            public void onLoadMore() {
                if(items.size() <= 50) // Change max size
                {
                    items.add(null);
                    adapter.notifyItemInserted(items.size()-1);
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            items.remove(items.size()-1);
                            adapter.notifyItemRemoved(items.size());

                            //Random more data
                            int index = items.size();
                            int end = index+10;
                            for(int i=index;i<end;i++)
                            {
                                String name = UUID.randomUUID().toString();
                                Item item = new Item(name,name.length());
                                items.add(item);
                            }
                            adapter.notifyDataSetChanged();
                            adapter.setLoaded();
                        }
                    },3000); // Time to load
                }else{
                    Toast.makeText(MainActivity.this, "Load data completed !", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    private void random10Data() {
        for(int i =0;i<10;i++)
        {
            String name = UUID.randomUUID().toString();
            Item item = new Item(name,name.length());
            items.add(item);
        }
    }
}

Ở đây thì mình tạo ra các data ngẫu nhiên bằng hàm random10Data

 private void random10Data() {
        for(int i =0;i<10;i++)
        {
            String name = UUID.randomUUID().toString(); // Tạo 1 chuỗi UUID ngẫu nhiên
            Item item = new Item(name,name.length()); // Tạo mới 1 item model
            items.add(item); // Add vào danh sách
        }
    }

Tạo adapter và set adapter cho recyclerView bình thường như Đan Trường những năm trước 2000 chưa nổi tiếng

   adapter = new MyAdapter(recycler,this,items);
        recycler.setAdapter(adapter);

Đây mới là cái ăn tiền nè , chúng ta phải implement cái Loadmore interface chứ không là nãy giờ bạn công cốc đấy

adapter.setLoadMore(new ILoadMore() {
            @Override
            public void onLoadMore() {
                if(items.size() <= 50) // Bạn có thể change max giá trị load ở đây , load tới số lượng như này thì có kéo nữa cũng không load nữa , bỏ điều kiện này đi thì nó cứ thế mà load
                {
                    items.add(null); // Add 1 cái null , để làm gì ? Quay lại cái Adapter của chúng ta mà thấy , nếu gặp item null thì nó sẽ coi đó là Loading View
                    adapter.notifyItemInserted(items.size()-1); // Báo với adapter là có sự thay đổi
                    new Handler().postDelayed(new Runnable() {  // Cái này là mình giả lập , bạn có thể replace cái Handler này với hàm fetch tới Web API của các bạn để load dữ liệu
                        @Override
                        public void run() {
                            items.remove(items.size()-1); // Remove thằng null khi nãy ra
                            adapter.notifyItemRemoved(items.size()); // Báo là có sự thay đổi

                            //Random dữ liệu
                            int index = items.size();
                            int end = index+10;
                            for(int i=index;i<end;i++)
                            {
                                String name = UUID.randomUUID().toString();
                                Item item = new Item(name,name.length());
                                items.add(item);
                            }
                            adapter.notifyDataSetChanged();
                            adapter.setLoaded();
                        }
                    },3000); // Mình delay 3 giây để demo , trong thực tế thì thời gian này dựa trên việc tải dữ liệu nhanh hay chậm
                }else{
                    Toast.makeText(MainActivity.this, "Load data completed !", Toast.LENGTH_SHORT).show();
                }
            }
        });

Okay và kết quả của chúng ta là