Retrofit - популярная и удобная библиотека, которая используется, если приложению необходимо работать с сервером. В этой статье я на очень простом примере рассмотрю основы работы с ней.
Предположим, что на каком-то сервере есть данные. И сервер готов нам выдать эти данные. Чаще всего для этого используется формат json. Вот пример таких данных: https://rawgit.com/startandroid/data/master/messages/messages1.json
Давайте используем Retrofit, чтобы получить в приложении данные из этого файла.
Для начала немного теории.
Конфигурирование Retrofit можно разделить на две части: API интерфейс и билдер. Давайте рассмотрим подробно, кто из них за что отвечает.
Представим себе работу по обмену данными приложения с сервером. Как это происходит? Обычно сервер предоставляет нам какое-то API, т.е. набор методов (он же REST). Если, например, это сервер интернет-магазина, то его API может содержать следующие методы:
getProducts - получить список товаров
getProdut - получить детальные данные о продукте
getOrders - получить список заказов
getOrder - получить детальные данные о заказе
createOrder - создать заказ
и т.д.
Это серверные методы и, чтобы их вызывать, нам необходимо выполнять запросы, которые могут выглядеть, например, так:
http://server/api/v1/getProducts
http://server/api/v1/getProduct
http://server/api/v1/getOrders
http://server/api/v1/getOrder
http://server/api/v1/createOrder
и т.д.
Т.е. у сервера есть какой-то базовый URL - http://server/api/v1/. И к нему просто добавляются имена методов (getProducts, getOrders, и т.д.), чтобы получать ссылки для работы с API методами.
Возвращаемся в приложение. Было бы очень удобно, если бы у нас в приложении был класс ServerApi с методами getProducts, getOrders и т.д. И при вызове этих методов происходил бы вызов соответствующих серверных методов. И Retrofit как раз может создать для нас такой класс. От нас потребуется интерфейс, в котором мы распишем необходимые методы.
Вот пример такого интерфейса:
public interface ServerApi { @GET("getProducts") List<Product> getProducts(); @GET("getProduct") Product getProduct(long productId); @GET("getOrders") List<Order> getOrders(); @GET("getOrder") Order getOrder(long orderId); @POST("createOrder") void createOrder(Order order); }
Этот код немного упрощен. В реальном примере будут дополнительные аннотации и обертки. Но общий смысл он вполне передает. Retrofit создаст класс, который реализует этот интерфейс. Внутри этого класса и будут сгенерированы вызовы к серверу.
А мы в этом интерфейсе можем настроить названия методов и некоторые HTTP штуки, такие как передаваемые параметры (Query), заголовки (Header) и т.п.
Рассмотрим описание метода getProducts подробнее:
@GET("getProducts") List<Product> getProducts();
Аннотация GET означает, что HTTP-запрос для getProducts должен быть типа GET. А в качестве параметра для этой аннотации нам необходимо указать имя метода на сервере. Обратите внимание, что здесь не фигурирует базовый URL (http://server/api/v1/). Его мы будем указывать позже, в билдере.
List<Product> - это тип возвращаемых данных. Retrofit сам сможет сконвертировать json данные в список объектов Product. Подробнее об этом тоже поговорим во время обсуждения билдера.
Ну и имя метода - getProducts(). В этом примере оно совпадает с именем метода на сервере. Но это необязательно. Т.е. вы вполне можете сделать так
@GET("getProducts") List<Product> getProductList();
Теперь имена методов различаются. Вы в коде будете вызывать метод getProductList, а на сервере будет вызываться getProducts.
Переходим к практике. Чтобы использовать в вашем проекте Retrofit, в build.gradle файле, в секции dependencies добавляйте зависимости:
compile 'com.squareup.retrofit2:retrofit:2.3.0' compile 'com.squareup.retrofit2:converter-gson:2.3.0'
Первая строка - это непосредственно Retrofit, а вторая - это его Gson-конвертер.
Напомню, что у нас есть ссылка: https://rawgit.com/startandroid/data/master/messages/messages1.json.
Она вернет нам json данные, которые представляют из себя список сообщений. Каждое сообщение содержит поля id, time, text и иногда image. Эта ссылка, конечно, не API, а просто текстовый файл, но смысл остается тем же, просто вместо имени метода у нас будет имя файла.
Мы делим ссылку на две части:
"https://rawgit.com/startandroid/data/master/messages/" - базовый URL (будет указан в билдере)
"messages1.json" - имя файла (будет указано в интерфейсе, в GET аннотации)
Мы можем создать интерфейс для работы с этой ссылкой. Там будет только один метод, и он будет возвращать List<Message>.
Давайте сначала создадим класс Message.
public class Message { private long id; private long time; private String text; private String image; // getters and setters ... }
Его поля соответствуют полям в данных в messages1.json.
Теперь создаем интерфейс:
import java.util.List; import retrofit2.Call; import retrofit2.http.GET; public interface MessagesApi { @GET("messages1.json") Call<List<Message>> messages(); }
В аннотации GET указываем, что к базовому URL надо будет добавить "messages1.json", чтобы получилась нужная нам ссылка.
Обратите внимание на Call. Эта обертка нужна для работы Retrofit. В ней мы указываем, какой тип данных ожидаем получить из messages1.json - т.е. List<Message>.
Интерфейс создан, теперь переходим к билдеру:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://rawgit.com/startandroid/data/master/messages/") .addConverterFactory(GsonConverterFactory.create()) .build();
В билдере мы указываем базовый URL и добавляем Gson конвертер, чтобы Retrofit сам смог сконвертировать json данные в объекты Message. Это минимум настроек, который от нас требуется для нашего простого примера. Для сложных случаев количество параметров может быть больше.
В итоге у нас есть объект Retrofit, который содержит базовый URL и способность преобразовывать json данные с помощью Gson. Мы передаем ему в метод create класс интерфейса, в котором описывали методы.
MessagesApi messagesApi = retrofit.create(MessagesApi.class);
И получаем реализацию MessagesApi от Retrofit. В этой реализации соединены настройки билдера (базовый URL и Gson конвертер) и описания методов из интерфейса (метод messages для получения файла messages1.json). В итоге Retrofit будет брать базовый URL из билдера, присоединять к нему остаток пути, который мы указываем в GET в интерфейсе, и тем самым получит полную ссылку.
Возможно, возникает вопрос, зачем было разделять функционал на билдер и интерфейс. Я могу привести вам пару примеров из своей практики.
1) У интернет-магазина есть два сервера: рабочий (с реальными данными) и тестовый. На обоих развернуто одинаковое API. И есть приложение, которое общается с сервером. Debug-версия приложения должна работать с тестовым сервером и выводить в лог всю информацию об этом. Release-версия работает с рабочим сервером и ничего не выводит в лог.
Эту разницу между Debug и Release версиями можно реализовать двумя разными билдерами. Просто указываем для них разные базовые URL и разные настройки логирования. А интерфейс в обоих случаях будет использоваться один и тот же.
2) Есть сервер с API. И есть приложение, которое вызывает эти методы. На сервере реализована авторизация. Т.е. приложение должно вызвать метод авторизации, получить токен, и затем использовать этот токен при вызове некоторых API методов, которые связаны с пользователем.
В билдере есть возможность использовать специальный обработчик, который будет добавлять токен ко всем вызовам. Но нам надо добавлять токен не ко всем, а только к некоторым вызовам.
В этом случае можно создать два билдера. Первый не будет использовать токен. Реализацию, полученную от этого билдера используем для работы с методами, не требующими токена. А второй билдер будет добавлять токен ко всем вызовам. Реализацию, полученную от этого билдера используем для всех методов, где нужен токен.
Итак, связка интерфейса и билдера дала нам реализацию MessagesApi. Используем ее для получения данных. Вызываем метод messages()
Call<List<Message>> messages = messagesApi.messages(); messages.enqueue(new Callback<List<Message>>() { @Override public void onResponse(Call<List<Message>> call, Response<List<Message>> response) { log("response " + response.body().size()); } @Override public void onFailure(Call<List<Message>> call, Throwable t) { } });
Метод log - это просто вывод в лог текста
Вызов метода messages вернет нам Call объект. При выполнении этого кода запрос к серверу еще не был отправлен. Call дает нам возможность отправить запрос синхронно (блокируя текущий поток) или асинхронно (с колбэком). Мы выбираем асинхронный вариант: вызываем метод enqueue и создаем для него Callback. Запрос будет выполнен в отдельном потоке, а результат придет в Callback в main потоке.
Если взаимодействие с сервером пройдет успешно, то мы получим результат в метод onResponse в объекте Response. Чтобы добраться до данных, необходимо вызвать метод body() у Response объекта. Это даст нам список сообщений - List<Message>. Выводим в лог количество записей в этом списке.
Запустив этот код, мы получим список из 50 записей. Лог покажет следующее:
response 50
Если по каким-то причинам не удалось достучаться до сервера, то вместо onResponse, будет вызван метод onFailure. Добавим вывод в лог ошибки, которую вернет нам Retrofit
@Override public void onFailure(Call<List<Message>> call, Throwable t) { log("failure " + t); }
Чтобы эмулировать ошибку, мы можем просто поломать имя сервера в базовом URL, который задается в билдере. Например, добавим букву r к rawgit:
https://rrawgit.com/startandroid/data/master/messages/
Запускаем и видим в логе ошибку.
failure java.net.UnknownHostException: Unable to resolve host "rrawgit.com": No address associated with hostname
Retrofit сообщает, что не нашел сервер rrawgit.com
Если случится какая-то ошибка в процессе работы Retrofit, то она попадет также в onFailure. Например, если сервер вернет некорректные данные, то GsonConverterFactory не сможет их распарсить и выкинет ошибку. А Retrofit отправит ее нам в onFailure.
Есть тип ошибок, который Retrofit умеет обрабатывать без вызова onFailure. Это HTTP-ошибки сервера. Самый простой случай - ошибка 404. Запрос дойдет до сервера, но сервер вместо данных вернет HTTP 404 Not found. Казалось бы - это ошибка и на должна прийти в onFailure. Но нет. onFailure вызывается в случае, когда не удалось достучаться до сервера. А здесь с сервером все ок, он просто вместо данных вернул нам информацию о том, что мы странные и хотим непонятного.
В итоге, ответ сервера мы получим в методе onResponse. И нам надо как-то различать, вернул сервер данные или ошибку. Для этого у объекта Response есть метод isSuccessful.
Перепишем метод onResponse
@Override public void onResponse(Call<List<Message>> call, Response<List<Message>> response) { if (response.isSuccessful()) { log("response " + response.body().size()); } else { log("response code " + response.code()); } }
Если запрос был выполнен успешно (isSuccessfull), то выводим в лог количество полученных записей. Иначе выводим в лог код ошибки.
Снова поменяем базовый URL, чтобы спровоцировать ошибку 404. Добавим лишнюю букву s в слово messages:
https://rawgit.com/startandroid/data/master/messagess/
Запускаем и в логе видим
response code 404
Чтобы получить полный текст ошибки сервера, используйте
response.errorBody().string()
Приведу весь код работы с Retrofit:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://rawgit.com/startandroid/data/master/messages/") .addConverterFactory(GsonConverterFactory.create()) .build(); MessagesApi messagesApi = retrofit.create(MessagesApi.class); Call<List<Message>> messages = messagesApi.messages(); messages.enqueue(new Callback<List<Message>>() { @Override public void onResponse(Call<List<Message>> call, Response<List<Message>> response) { if (response.isSuccessful()) { log("response " + response.body().size()); } else { log("response code " + response.code()); } } @Override public void onFailure(Call<List<Message>> call, Throwable t) { log("failure " + t); } });
Разумеется, вам не нужно при каждом запросе к серверу использовать билдер и создавать новый объект MessagesApi. Это делается один раз за время работы приложения и далее объект MessagesApi используется везде, где необходима работа с сервером. Для реализации Singleton подойдет Dagger или любая другая реализация этого паттерна.
На этом пока все, чтобы не перегружать вас информацией. Продолжение в следующем посте.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня