Изучаем Retrofit 2

  • Tutorial


В мире Android разработки существует множество интересных библиотек, и сегодня мы рассмотрим детище компании SquareRetrofit. Что же это за зверь такой? Retrofit (согласно официальному сайту) — типобезопасный HTTP-клиент для Android и Java. Он является незаменимым инструментом для работы с API в клиент-серверных приложениях. Каких-то лет 5 назад Android-разработчикам для работы с сетью приходилось воротить горы кода с обратными вызовами, AsyncTask'ами и прочими «низкоуровневыми» вещами. И компания Square выпустила такую замечательную библиотеку — Retrofit.

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

Лучше один раз увидеть, чем сто раз услышать


Мы будем создавать приложение, получающее данные от API сайта Umorili, так как только они предоставляют данные с баша в удобном для парсинга виде. Вот так будет выглядеть конечный вариант:

Дизайном, конечно, не блещет
Ну что, дети, вы готовы?

Зависимости


Библиотеку Retrofit можно подключить тремя способами: Gradle, Maven, Jar. Опишем каждый способ.

Gradle


В большинстве случаев для сборки приложений под Android используется именно этот инструмент, поэтому, если вы не уверены, берите этот вариант :) (здесь и далее для зависимостей будут использоваться Gradle).

Для подключения в файл build.gradle модуля приложения в раздел dependencies вставляем строчку:

compile 'com.squareup.retrofit2:retrofit:2.1.0'

Maven


Если кто-то использует эту систему зависимостей и сборки, то фрагмент зависимости будет выглядеть так:

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>2.1.0</version>
</dependency>

Jar


Не приветствую использование этого варианта, но некоторые любят его. Скачиваем с официального сайта jar-файл (ссылка) и кидаем его в папку libs.

Помимо самой библиотеки нам понадобится парсер JSON и RecyclerView-v7, поэтому подключим их:

compile 'com.squareup.retrofit2:converter-gson:2.1.0' //Конвертер JSON, можно, если предпочитаете, использовать Jackson
compile 'com.android.support:recyclerview-v7:25.0.0' //RecyclerView

С зависимостями разобрались, теперь перейдем к самой сладкой части — разработке. Перво-наперво нам нужно описать запросы к API, поэтому.

Описание запросов к API


Retrofit позволяет сделать полноценный REST-клиент, который может выполнять POST, GET, PUT, DELETE. Для обозначения типа и других аспектов запроса используются аннотации. Например, для того, чтобы обозначить, что требуется GET запрос, нам нужно написать перед методом GET, для POST запроса POST, и так далее. В скобках к типу запроса ставится целевой адрес. Для примера возьмем API GitHub'а. Полный URL для получения списка репозиториев определенного пользователя можно представить в виде https://api.github.com/users/octocat/repos, где:

  • api.github.com — базовая часть адреса (всегда оканчивается слешем)
  • users/{user}/repos — метод (адрес документа, целевой адрес), где определенного пользователя (octocat) мы заменили на алиас (про использование алиасов чуть позже)

Еще существуют параметры запроса, например в запросе к Umorili мы будем использовать следующий адрес — https://umorili.herokuapp.com/api/get?name=bash&num=50, где name=bash&num=50 — параметры.

Но одними аннотациями описание не заканчивается, нам же надо где-то их описать. А описываем мы их в интерфейсе (interface). Для нашего приложения интерфейс будет следующим:

package ru.mustakimov.retrofittutorial.api;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import ru.mustakimov.retrofittutorial.PostModel;

public interface UmoriliApi {
    @GET("/api/get")
    Call<List<PostModel>> getData(@Query("name") String resourceName, @Query("num") int count);
}

Разберем этот интерфейс. У нас есть метод getData, возвращающий объект типа Call<List<PostModel>>. Методы должны всегда возвращать объект типа Call<T> и иметь аннотацию типа запроса (GET, POST, PUT, DELETE).

Аннотация @Query("name") String resourceName показывает Retrofit'у, что в качестве параметра запроса нужно поставить пару name=<Значение строки resourceName>.

Если у нас в целевом адресе стоит алиас, то для того, чтобы заместо алиаса поставить значение, нам нужно в параметрах функции написать @Path("<Название аласа>") SomeType variable, где SomeType — любой тип (например, String, int, float).

PostModel — класс, сгенерированный сайтом jsonschema2pojo на основе ответа сервера.

Вот сам класс
package ru.mustakimov.retrofittutorial;

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

public class PostModel {

    @SerializedName("site")
    @Expose
    private String site;
    @SerializedName("name")
    @Expose
    private String name;
    @SerializedName("desc")
    @Expose
    private String desc;
    @SerializedName("link")
    @Expose
    private String link;
    @SerializedName("elementPureHtml")
    @Expose
    private String elementPureHtml;

    /**
     * @return The site
     */
    public String getSite() {
        return site;
    }

    /**
     * @param site The site
     */
    public void setSite(String site) {
        this.site = site;
    }

    /**
     * @return Site name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name Site name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return Site description
     */
    public String getDesc() {
        return desc;
    }

    /**
     * @param desc Site description
     */
    public void setDesc(String desc) {
        this.desc = desc;
    }

    /**
     * @return The link
     */
    public String getLink() {
        return link;
    }

    /**
     * @param link The link
     */
    public void setLink(String link) {
        this.link = link;
    }

    /**
     * @return The elementPureHtml
     */
    public String getElementPureHtml() {
        return elementPureHtml;
    }

    /**
     * @param elementPureHtml The elementPureHtml
     */
    public void setElementPureHtml(String elementPureHtml) {
        this.elementPureHtml = elementPureHtml;
    }

}


Подготовка к запросу


Перед отправкой запроса и получением результата нам нужно произвести инициализацию Retrofit'а и объекта интерфейса. Чтобы приложение не имело сотню объектов, выполняющих одну и ту же функцию, мы произведем всю инициализацию в классе, унаследованном от Application. Код тогда будет следующим:

package ru.mustakimov.retrofittutorial;

import android.app.Application;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import ru.mustakimov.retrofittutorial.api.UmoriliApi;

public class App extends Application {

    private static UmoriliApi umoriliApi;
    private Retrofit retrofit;

    @Override
    public void onCreate() {
        super.onCreate();

        retrofit = new Retrofit.Builder()
                .baseUrl("https://umorili.herokuapp.com") //Базовая часть адреса
                .addConverterFactory(GsonConverterFactory.create()) //Конвертер, необходимый для преобразования JSON'а в объекты
                .build();
        umoriliApi = retrofit.create(UmoriliApi.class); //Создаем объект, при помощи которого будем выполнять запросы
    }

    public static UmoriliApi getApi() {
        return umoriliApi;
    }
}
P.S. не забываем в манифесте прописать, что используем свой класс Application

Теперь из любого класса мы имеем доступ к API.

Получение данных


Мы можем выполнять запросы (и, следовательно, получать данные) двумя способами — синхронными а асинхронными запросами. Для синхронного (блокирующего) получения мы используем метод execute() у объекта типа Call. Для нашего примера код был бы следующим:

Response response = App.getApi().getData("bash", 50).execute();

В результате выполнения мы получаем объект типа Response (ответ), откуда мы можем уже получить распарсенный ответ методом body().

Для асинхронного получения мы заменяем execute() на enqueue(), где в параметрах передаем функции обратного вызова (колбэки). В нашем примере будет выглядеть примерно так:

App.getApi().getData("bash", 50).enqueue(new Callback<List<PostModel>>() {
    @Override
    public void onResponse(Call<List<PostModel>> call, Response<List<PostModel>> response) {
        //Данные успешно пришли, но надо проверить response.body() на null
    }
    @Override
    public void onFailure(Call<List<PostModel>> call, Throwable t) {
        //Произошла ошибка
    }
});

Делаем отображение данных


Данные мы уже получили, а как и теперь отобразить? Кидаем в разметку активности RecyclerView и как-нибудь его обзываем. После этого создаем разметку для элемента.

Вот что получилось у меня
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ru.mustakimov.retrofittutorial.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:id="@+id/posts_recycle_view"
        android:layout_alignParentStart="true" />
</RelativeLayout>

post_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="5dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/postitem_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Очень интересный пост с баша, который никто никогда не видел, так как его не существует"
        android:textColor="?android:attr/textColorPrimary"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <TextView
        android:id="@+id/postitem_site"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bash.im"
        android:layout_below="@+id/postitem_post"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:gravity="end"
        android:textAlignment="textEnd" />
</RelativeLayout>


После создаем адаптер для RecyclerView:

Код адаптера
package ru.mustakimov.retrofittutorial;

import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.List;

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

    private List<PostModel> posts;

    public PostsAdapter(List<PostModel> posts) {
        this.posts = posts;
    }

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

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        PostModel post = posts.get(position);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            holder.post.setText(Html.fromHtml(post.getElementPureHtml(), Html.FROM_HTML_MODE_LEGACY));
        } else {
            holder.post.setText(Html.fromHtml(post.getElementPureHtml()));
        }
        holder.site.setText(post.getSite());
    }

    @Override
    public int getItemCount() {
        if (posts == null)
            return 0;
        return posts.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        TextView post;
        TextView site;

        public ViewHolder(View itemView) {
            super(itemView);
            post = (TextView) itemView.findViewById(R.id.postitem_post);
            site = (TextView) itemView.findViewById(R.id.postitem_site);
        }
    }
}


И прописываем в MainActivity.java инициализацию RecyclerView, адаптера, а так же получение данных.

А вот и MainActivity
package ru.mustakimov.retrofittutorial;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.Toast;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    List<PostModel> posts;

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

        posts = new ArrayList<>();

        recyclerView = (RecyclerView) findViewById(R.id.posts_recycle_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);

        PostsAdapter adapter = new PostsAdapter(posts);
        recyclerView.setAdapter(adapter);

        try {
            Response response = App.getApi().getData("bash", 50).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }

        App.getApi().getData("bash", 50).enqueue(new Callback<List<PostModel>>() {
            @Override
            public void onResponse(Call<List<PostModel>> call, Response<List<PostModel>> response) {
                posts.addAll(response.body());
                recyclerView.getAdapter().notifyDataSetChanged();
            }

            @Override
            public void onFailure(Call<List<PostModel>> call, Throwable t) {
                Toast.makeText(MainActivity.this, "An error occurred during networking", Toast.LENGTH_SHORT).show();
            }
        });
    }
}


На GitHub'е вы можете найти полный код данного приложения.
  • +11
  • 123k
  • 6
Поделиться публикацией

Комментарии 6

    +1
    Огромное спасибо за материал! Только вчера пытался разобраться по документации с их сайта, что оказалось достаточно сложной задачей для дилетанта.
      0
      Может, для начала, стоит научиться «ходить в сеть» без Retrofit-а?
        0
        Может и стоит, но кроме скачивания URL по методу GET простого ничего нет.
          0
          после «скачивания URL» полученный поток прекрасно десериализуется, если знать как он сериализован на сервере. В эпоху сервлетов так и работали ).
          0
          Мне кажется он может :)
        0
        То чувство, когда запустил предоставленный код и залип над постами с баша, забыв про приложение >_<

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

        Самое читаемое