В мире Android разработки существует множество интересных библиотек, и сегодня мы рассмотрим детище компании Square — Retrofit. Что же это за зверь такой? 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 мы будем использовать следующий адрес —
http://www.umori.li/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("http://www.umori.li/") //Базовая часть адреса
.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
post_item.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'е вы можете найти полный код данного приложения.
Поделиться с друзьями
Ramiel2009
Огромное спасибо за материал! Только вчера пытался разобраться по документации с их сайта, что оказалось достаточно сложной задачей для дилетанта.
gshock
Может, для начала, стоит научиться «ходить в сеть» без Retrofit-а?
Cosmonaut
Может и стоит, но кроме скачивания URL по методу GET простого ничего нет.
supervisor
после «скачивания URL» полученный поток прекрасно десериализуется, если знать как он сериализован на сервере. В эпоху сервлетов так и работали ).
supervisor
Мне кажется он может :)