Поиск по этому блогу

вторник, 8 августа 2017 г.

Пагинация для локального ArrayList

Недавно понадобилось делать пагинацию подручными способами, пришлось выкручиваться как только можно, так что сегодня я поделюсь своим опытом создания пагинации из локального списка. Это в принципе совсем не сложно и вполне можно самому додуматься как такое сделать, но в интернете я не нашел статей, так что думаю кому-то да пригодится.


Вот так вот будет выглядеть наша пагинация, она работает так потому что я специально сделал задержку перед загрузкой и кажется что данные подгружаются откуда-то с удаленного сервера, а на деле оно все хранится локально у нас в списке. Если же убрать эту задержку данные просто будут загружаться так быстро как вы приближаетесь к его концу.

Для начала хочу сказать что в этом проекте я использовал уже возможно знакомый для вас код, так как я взял его из одной своей статьи: Работа с Retrofit. Оттуда я взял собственно код запроса, частично код адаптера и модели для респонса. И просто разбил этот список который нам возвращает апи на несколько частей. В приложении я сделал так что бы оно загружало по 10 итемов каждый раз когда мы скролим в низ. То есть мы дошли до низа списка, и делаем «запрос» на получение еще 10 из списка и так пока у нас не закончатся итемы.

Давайте начнем. По старинке для начала настроим наш проект для работы, нам нужно добавить библиотеки и настроить манифест для работы с интернетом. 

app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.project.dajver.listpaginationexample"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'

    compile 'com.jakewharton:butterknife:8.8.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.0'

    compile 'com.squareup.retrofit2:retrofit:2.0.1'
    compile 'com.squareup.retrofit2:converter-gson:2.0.1'
    compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
    compile 'com.squareup.retrofit2:converter-scalars:2.0.1'

    compile 'com.android.support:recyclerview-v7:25.0.+'
}

У нас в проекте будет две основных библиотеки — это Retrofit и ButterKnife, первая нам нужна для создания реквестов на апи, вторая для упрощенного подключения вьюх к активитям. А еще у нас будет одна библиотека RecyclerView для списка.

Далее давайте настроим манифест. В нем нам всего лишь надо прописать пермишен доступа в интернет.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.project.dajver.listpaginationexample">

    <uses-permission android:name="android.permission.INTERNET" />

    <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>

Ну, а дальше давайте приступим к кодингу. С начала напишем всю работу с апи, оно у нас стандартное как и весь код с ретрофитом. У нас есть интерфейс в котором у нас все реквесты к апи, класс который описывает эти запросы и модель которая принимает респонс с сервера и парсит ее.

Для начала давайте создадим модель. В ней у нас будет всего лишь один ArrayList который будет в себе хранить все данные которые возвращаются нам с сервера.

SearchModel.java
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.util.ArrayList;
import java.util.List;

public class SearchModel {
    @SerializedName("list")
    @Expose
    private List<List<String>> list = new ArrayList<>();

    public List<List<String>> getList() {
        return list;
    }

    public void setList(List<List<String>> list) {
        this.list = list;
    }
}

Вот такой вот класс. Нам нужен всего лишь этот класс, его нам будет достаточно для парсинга данных.

Дальше нам нужно описать интерфейс запросов. У нас он короткий, всего один запрос который мы будем вызывать далее.

API.java
import com.project.dajver.listpaginationexample.api.model.SearchModel;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface API {
    @GET("api.php?method=search")
    Call<SearchModel> searchAudio(@Query("q") String query, @Query("key") String key);
}

Тут как видно мы делаем get запрос на какой-то адрес и дальше в коде указываем что принимать мы будем SearchModel, а отправлять название песни которую мы ищем и ключ который нам выдали для работы с апи сайт с которого я взял это АПИ.

Дальше нам нужно написать сервис который будет создавать запросы с нашего интерфейса, так сказать будет описывать работу нашего интерфейса.

RestClient.java
import com.google.gson.Gson;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;

public class RestClient {
    public static final String BASE_URL = "http://api.xn--41a.ws/";
    public static final String API_KEY = "711b23b60ff8da0c3aa2451ab3a6beb9";

    private static final RestClient instance = new RestClient();

    public static API instance() {
        return instance.service;
    }

    public static Gson gson() {
        return new Gson();
    }

    private final API service;

    public RestClient() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(logLevel())
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        service = retrofit.create(API.class);
    }

    private static OkHttpClient logLevel() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .build();
        return client;
    }
}

Собственно тут мы в константах описали адрес сайта к которому будем обращаться за АПИ, указали ключ который мы будем использовать как токен и дальше мы создаем статический инстанс нашего класса что бы нам проще было обращаться к паблик методам этого класса. Далее в конструкторе инициализируем ретрофит, подключаем к нему фабрики для конверта данных и указываем какой класс будем использовать для создания запросов.

Ниже в методе logLevel() мы говорим что мы хотим видеть все логи которые есть во время запросов и когда запрос прекращает свою работу.

Дальше нам нужно создать адаптер который мы будем использовать для отображения списка песен. 

MusicRecyclerAdapter.java
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.project.dajver.listpaginationexample.R;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

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

    private List<List<String>> searchModels = new ArrayList<>();

    public void add(List<String> string) {
        searchModels.add(string);
        notifyDataSetChanged();
    }

    @Override
    public MusicRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music, parent, false);
        MusicRecyclerAdapter.ViewHolder pvh = new MusicRecyclerAdapter.ViewHolder(v);
        return pvh;
    }

    @Override
    public void onBindViewHolder(final MusicRecyclerAdapter.ViewHolder holder, final int position) {
        holder.title.setText(searchModels.get(position).get(4) + " - " + searchModels.get(position).get(3));
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.textView)
        TextView title;

        ViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }
    }
}

В целом стандартный адаптер, единственное что интересное это метод add(), он у нас для сетта данных в адаптер, в него мы передаем List и как бы потом в onBindViewHolder() мы его парсим по позиции так как этот список имеет статичное количество параметров и он содержит в себе по определенным полям определенные данные которые нам интересны. Собственно по id 4 и id 3 мы получаем имя исполнителя и название песни.

Вот так будет выглядеть отдельный айтем для адаптера.

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

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="15dp"
        android:text="TextView"
        android:textSize="18sp" />

</LinearLayout>

Еще нам не хватает одного класса который будет добавлять айтемы с задержкой. Его мы реализовали как AsyncTask в котором будем делать задержку на 3 секунды и потом возващать колбек в активити.

AddItemsTask.java
import android.os.AsyncTask;

import java.util.concurrent.TimeUnit;

public class AddItemsTask extends AsyncTask<Void, Void, Void> {

    private OnAddItemListener addItemToAdapter;

    public AddItemsTask(OnAddItemListener addItemToAdapter) {
        this.addItemToAdapter = addItemToAdapter;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        addItemToAdapter.onStartTask();
    }

    @Override
    protected Void doInBackground(Void... params) {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Void result) {
        super.onPostExecute(result);
        addItemToAdapter.onFinishTask();
    }

    public interface OnAddItemListener {
        void onStartTask();
        void onFinishTask();
    }
}

Вполне себе стандартный код как по мне, в onPreExecute() мы вызываем колбек который возвращает в активити колбек и у нас будет стартовать прогрес диалог который будет показывать что данные загружаются. Дальше в методе doInBackground() мы делаем задержку на 3 секунды, и в onPostExecute() мы вызываем колбек который добавляет данные в адаптер и прячет прогрес диалог.

Дак, адаптер и класс который загружает данные у нас есть, теперь нужно соеденить все кусочки в одно целое в нашей активити. В ней мы проинициализируем все нужные классы, подключим все методы, запустим адаптер и сделаем запрос на сервер для получения музыки.

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
import android.widget.ProgressBar;

import com.project.dajver.listpaginationexample.adapter.MusicRecyclerAdapter;
import com.project.dajver.listpaginationexample.api.RestClient;
import com.project.dajver.listpaginationexample.api.model.SearchModel;
import com.project.dajver.listpaginationexample.task.AddItemsTask;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity implements Callback<SearchModel>, AddItemsTask.OnAddItemListener {

    @BindView(R.id.recycleView)
    RecyclerView recycleView;
    @BindView(R.id.progressBar)
    ProgressBar progressBar;

    public int NUM_ITEMS_PAGE = 10;

    private MusicRecyclerAdapter musicRecyclerAdapter;
    private List<List<String>> listList;
    private int paginationCounter = 0;

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

        recycleViewSetup(recycleView);
        musicRecyclerAdapter = new MusicRecyclerAdapter();
        recycleView.setAdapter(musicRecyclerAdapter);
        recycleView.addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                LinearLayoutManager layoutManager = LinearLayoutManager.class.cast(recyclerView.getLayoutManager());
                int totalItemCount = layoutManager.getItemCount();
                int lastVisible = layoutManager.findLastVisibleItemPosition();

                boolean endHasBeenReached = lastVisible + 5 >= totalItemCount;
                if (totalItemCount > 0 && endHasBeenReached) {
                    new AddItemsTask(MainActivity.this).execute();
                }
            }
        });

        RestClient.instance().searchAudio("Armin", RestClient.API_KEY).enqueue(this);
    }

    public void recycleViewSetup(RecyclerView recyclerView) {
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
    }

    @Override
    public void onResponse(Call<SearchModel> call, Response<SearchModel> response) {
        this.listList = response.body().getList();
        addItemToAdapter();
    }

    @Override
    public void onFailure(Call<SearchModel> call, Throwable t) {
        t.printStackTrace();
    }

    private void addItemToAdapter() {
        paginationCounter += NUM_ITEMS_PAGE;
        for (int i = paginationCounter - NUM_ITEMS_PAGE; i < paginationCounter; i++) {
            progressBar.setVisibility(View.VISIBLE);
            if (i < listList.size() && listList.get(i) != null)
                musicRecyclerAdapter.add(listList.get(i));
        }
    }

    @Override
    public void onStartTask() {
        progressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void onFinishTask() {
        addItemToAdapter();
        progressBar.setVisibility(View.GONE);
    }
}

В методе onCreate() мы инициализируем леяут, ButterKnife, так же в этом методе мы инициализируем RecyclerView, создаем адаптер и сетим его в наш RecyclerView, и добавляем onScrollListener и в нем проверяем дошли ли мы до конца списка, и если дошли то запускаем наш AddItemsTask который по истечению 3 секунд добавит новые данные с помощью колбека onFinishTask(). Ну и в самом низу этого метода мы делаем запрос на сервер.

Метод recycleViewSetup() нам нужен для настройки RecyclerView, без этого метода наш список просто не отобразится на экране. В нем мы задаем размер, расположение, количество колонок и т.д., для нашего списка.

Методы onResponse() и onFailure() стандартные методы Retrofit, в первом мы сетим данные в список для дальнейшей работы с ним и вызываем метод который сетит часть данных, а именно 10 итемов в адаптер.

Метод addItemToAdapter() нам нужен для создания пагинации, в нем мы берем наш каунтер и прибавляем ему NUM_ITEMS_PAGE который равен у нас 10, собственно 10 итемам которые мы в начале будем отображать. Дальше в цикле мы проходимся paginationCounter количество раз с шагом paginationCounter — NUM_ITEMS_PAGE, это мы делаем для того что бы у нас всегда загружались только с нужного шага данные, то есть если мы на 5 шаге то у нас будет грузиться с 5 по 6 так как paginationCounter = 60, а NUM_ITEMS_PAGE = 10, а в сумме одно минус другое дает нам общее количество итемов которые нам нужно загрузить в адаптер. Потом мы останавливаем прогресс бар, проверяем что бы у нас не дай бог не выскачил IndexOfBoundsException и добавляем айтем в адаптер.

Ну и дальше идут два наших колбека onStartTask() и onFinishTask() которые выполняют собственно два важных события, делания прогресс бара видимым и прятание его с последующим вызовом метода addItemToAdapter().

Наша разметка будет выглядеть очень просто, в ней у нас будет всего лишь RecycerView и ProgressBar.

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="0.1">

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

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone" />

</LinearLayout>

И собственно все, у нас готов наш проект, включаем и наслаждаемся его работой. У тех кто дошел до этого момента должен случиться эстетически оргазм от того что все работает и все так красиво написанно. Возможно у кого-то конечно есть предложения по улучшению кода, я всегда за, предлагайте.


Исходники:

GitHub