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

четверг, 25 мая 2017 г.

Работа с Retrofit


image
Сталкиваясь с разными проектами я заметил, что большинство до сих пор используют разные библиотеки, которые реализуют запросы внутри UI или вообще через AsyncTask. Это как по мне глупо, так как для реализации API в приложении есть такая чудесная библиотека, как Retrofit 2.

По данной библиотеке существует огромное количество статей, которые в полной мере описывают работу с ней. Я опишу только то, как я с ней работаю, потому что не знаю почему, но большинство туториалов используют много лишнего кода при работе с ней. 

Приведу описание библиотеки из вики:
Retrofit является REST-клиентов для безопасной работы в Android и Java. Библиотекой удобно пользоваться для запроса к различным веб-сервисам с командами GET, POST, PUT, DELETE. Может работать в асинхронном режиме, что избавляет от лишнего кода.


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


Начнем с того что нам нужно подключить библиотеку в Build Gradle файле. Вот так выглядит мой:

app/build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'

    compile 'com.jakewharton:butterknife:8.0.1'
    apt 'com.jakewharton:butterknife-compiler:8.0.1'

    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.+'
}

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

Дальше нам нужно создать интерфейс, в котором опишем наши запросы, у нас он будет один. Мы будем искать музыку из ВКонтакте с api.xn--41a.ws этого сайта. Интерфейс мы создаем для того, чтобы определить нужные нам ссылки к API и какие параметры мы будем посылать.

Также как я писал ранее, у нас есть несколько типов запросов GET, POST, PUT и DELETE. Эти запросы получают разные параметры для их работы.

@GET — принимает параметры с @Query, @Path и @QueryMap.
Примеры:
@GET("group/{id}/users")
Call<List<User>> getList(@Path("id") int groupId, @Query("q") String q, @QueryMap Map<String, String> options);

@POST, @PUT, @DELETE — принимает параметры типа @Part, @Field и @Body. Тут есть еще один нюанс. @Part мы можем использовать только с параметром @Multipart, а @Field с параметром @FormUrlEncoded. Наоборот нельзя, потому что они работают только в паре. 

Пример:
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last, @Body someBody);

Собственно с параметрами разобрались, теперь смотрим что у нас будет в нашем интерфейсе. В нем у нас один единственный параметер @GET, и колбек который возвращает SearchModel. SearchModel же - это класс который сгенерирован с помощью сайта www.jsonschema2pojo.org, тут просто вставляем json и там он нам сгенерирует нужные переменные. Также там мы задаем параметры, которые мы будем отправлять вместе с ссылкой.

API.java
import com.project.retrofitexample.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);
}

А вот тут у нас SearchModel, который мы сгенерировали. Он нам будет возвращать список List<List> в котором нам возвращают нужные данные для отображения названия. К сожалению этот json очень хреново сделан, там параметры хранятся в StringArray, через запятую вместо параметров, но это нам не помеха:)

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

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

/**
 * Created by gleb on 5/24/17.
 */

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

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

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

import java.util.concurrent.TimeUnit;

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

/**
 * Created by gleb on 5/24/17.
 */

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()) //конвертер для json
                .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)
                .readTimeout(60, TimeUnit.SECONDS)
                .connectTimeout(60, TimeUnit.SECONDS)
                .build();
        return client;
    }
}

Вот так вот выглядит наш класс. 
Дальше нам нужно создать адаптер для RecycleView в котором мы отобразим наши данные с респонса, который нам вернется из нашего респонса, который мы получим с помощью Retrofit. О, как закручено получилось :)

MusicRecycleList.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.retrofitexample.R;

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

import butterknife.BindView;
import butterknife.ButterKnife;

/**
 * Created by gleb on 5/24/17.
 */

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

    // наш аррей с списком песен которые нам вернутся с апи
    private List<List<String>> searchModels = new ArrayList<>();
    //колбек который по клику будет что то делать, сами решайте что делать с ним
    private OnItemClickListener onItemClickListener;

    // сетим в конструкторе наш список песен
    public MusicRecycleList(List<List<String>> searchModels) {
        this.searchModels = searchModels;
    }

    // сетим вьюху и втю холдер
    @Override
    public MusicRecycleList.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_music, parent, false);
        MusicRecycleList.ViewHolder pvh = new MusicRecycleList.ViewHolder(v);
        return pvh;
    }

    // биндим данные в вьюху, в нашем случае в textview
    @Override
    public void onBindViewHolder(final MusicRecycleList.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);
            // указываем баттернайфу что мы получаем айдишники из item_music
            ButterKnife.bind(this, itemView);
            //отлавливаем клик по айтему
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    // отправляем колбек по клику на айтем, отлавливаем его в MainActivity
                    int position = MusicRecycleList.ViewHolder.super.getAdapterPosition();
                    onItemClickListener.onItemClick(position);
                }
            });
        }
    }

    // сеттер для колбека
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    // интерфейс колбек
    public interface OnItemClickListener {
        void onItemClick(int id);
    }
}


А вот так будет выглядеть item_music, который является айтемом для кастомного списка.

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>

Дальше нам нужно перейти в MainActivity и создать и проинициализировать наш RestClient, тоже самое надо сделать с адаптером в onResponse и тогда у нас все заработает как часы. В общем в нашем XML будет всего два элемента, это EditText и RecycleView. В первый мы будем вводить текст, во втором будем выводить список аудио.

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

    <EditText
        android:id="@+id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="@string/search_text"
        android:inputType="textPersonName" />

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

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

</LinearLayout>

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.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;

import com.project.retrofitexample.adapter.MusicRecycleList;
import com.project.retrofitexample.api.RestClient;
import com.project.retrofitexample.api.model.SearchModel;

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

public class MainActivity extends AppCompatActivity implements TextWatcher,
        Callback<SearchModel>,MusicRecycleList.OnItemClickListener {

    //биндим вьюхи 
    @BindView(R.id.searchView)
    EditText searchView;

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

    // проинициализировали адаптер
    MusicRecycleList musicRecycleList;

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

        // сетапим размеры и установик для ресайкла
        recycleViewSetup(recycleView);
        //а тут инициализируем текст лисенера для отслеживания ввода текста
        searchView.addTextChangedListener(this);
    }

    //настройки для recycle view
    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 beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        // тут у нас запрос к апи для получения аудио, передаем текст который вводим и ключ для апи
        RestClient.instance().searchAudio(charSequence.toString(), RestClient.API_KEY).enqueue(this);
    }

    @Override
    public void afterTextChanged(Editable editable) { }

    //респонс который мы получаем с апи
    @Override
    public void onResponse(Call<SearchModel> call, Response<SearchModel> response) {
        // инициализируем адаптер
        musicRecycleList = new MusicRecycleList(response.body().getList());
        //инициализируем он клик
        musicRecycleList.setOnItemClickListener(this);
        //сетим адаптер в ресайкл вью
        recycleView.setAdapter(musicRecycleList);
    }

    @Override
    public void onFailure(Call<SearchModel> call, Throwable t) {
        //выводим в лог ошибки
        t.printStackTrace();
    }

    @Override
    public void onItemClick(int id) {
        //TODO: здесь какое-то действие по клику
    }
}

Ну и добавляем в файл string.xml строку для edittext для того что бы в hint отображалось что-то, когда он пустой.


<string name="search_text">Search text here</string>

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

image


Исходники:

GitHub