Привет! В этой статье мы будем разбираться как работать с библиотекой Retrofit, которая призвана значительно сократить трудозатраты при работе с API веб-сервисов, а также напишем простой пример использования Retrofit 2 в тестовом приложении.
1.Retrofit. Что это?
Retrofit – это REST клиент для android и Java от компании Square. Он может относительно легко получать и разбирать JSON (или другие структуированные данные) через вебсервисы, использующие REST. В Retrofit для (де)сериализации данных используются конверторы, которые необходимо указывать вручную. Типичным конвертором для JSON формата является библиотека GSon, но вы можете воспользоваться кастомным конвертером для обработки XML или прочих протоколов. Для HTTP запросов Retrofit использует OkHttp библиотеку.
Вы можете создать Java объекты основанные на JSON через сервис по ссылке http://www.jsonschema2pojo.org/
Для работы с Retrofit нам потребуется выполнить три шага:
1)Создать класс модели, который будет перегоняться в JSON
2)Создать интерфейс, определяющий возможные HTTP Операции (API)
3)Настроить Retrofit с помощью Retrofit.Builder класса.
Сейчас наверняка вам ничего не понятно, но далее на примерах всё встанет на свои места.
Каждый метод из интерфейса (созданного на шаге 2) представляет одну реализацию вызова API вебсервиса. Метод должен иметь HTTP аннотацию(GET, POST, и т.д.) для указания типа запроса и URL адрес. Возвращаемое значение оборачивается в ответе в Call объект, параметризованный типом ожидаемого результата(нашей моделью из 1 шага).
@GET("/cats") Call<List<Cat>> getCats()
Вы можете менять части и параметры URL для настройки запроса. Замена части URL выполняется с помощью фигурных скобок {} . С помощью аннотации @Path мы связываем значение параметра метода со значtнием из скобок {} в аннотации.
@GET("users/{name}/commits") Call<List<Commit>> getCommitsByName(@Path("name") String name)
Для того что бы выполнить запрос с параметрами их необходимо добавить с аннотацией @Query в сигнатуру метода, в результате чего при вызове они добавятся в конец URL
@GET("users") Call<User> getUserById(@Query("id") Integer id)
Аннотация @Body на параметре метода заставит Retrofit использовать объект как тело запроса.
@POST("users") Call<User> postUser(@Body User user)
2.Retrofit конверторы и адаптеры.
2.1 Конверторы.
Retrofit может быть настроен с использованием специфичных конверторов. Задача конверторов – выполнить (де)сериализацию объектов. Некоторые конверторы для различных преобразований данных:
Для преобразования в JSON и обратно:
- Gson: com.squareup.retrofit:converter-gson
- Jackson: com.squareup.retrofit:converter-jackson
- Moshi: com.squareup.retrofit:converter-moshi
Для работы с протоколами сереализации (Protocol Buffers):
- Protobuf: com.squareup.retrofit:converter-protobuf
- Wire: com.squareup.retrofit:converter-wire
Ну и для работы с XML (куда же без него…):
Simple XML: com.squareup.retrofit:converter-simplexml
Ну а если вам захотелось каких то особых извращений или пришлось работать с неким специфичным протоколом – всегда можно создать собственный конвертор, унаследовавшись от класса Converter.Factory
2.2 Retrofit адаптеры
Ретрофит можно расширить адаптерами для связи с такими библиотеками как RxJava 2.x, Java 8 и Guava.
С выбором адаптеров можете ознакомиться на Гитхабе square/retrofit/retrofit-adapters/
Для примера RxJava 2.x адаптер может быть добавлен с помощью Gradle:
compile 'com.squareup.retrofit2:adapter-rxjava2:latest.version'
Или используя Maven
<dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>adapter-rxjava2</artifactId> <version>latest.version</version> </dependency>
Добавление адаптера в коде выглядит примерно таким методом
retrofit2.Retrofit.Builder.addCallAdapterFactory(Factory)
Точнее :
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.example.com").addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build();
С этим адаптером будут добавлены Retrofit интерфейсы, способные возвращать RxJava 2.x типы, например Observable, Flowable или Single и т.д.
@GET("users")Observable<List<User>> getUsers();
-
Аутентификация в Retrofit
Аутентификация может производиться как используя логин с паролем(Http Basic authentication), так и с применением токенов.
Существует 2 пути для обработки процедуры аутентификации. Первый – использование заголовков с помощью аннотаций. Второй – использование интерцепторов OkHttp. Посмотрим как с этим работать.
3.1 Аутентификация с помощью аннотаций.
Предположим, что вам необходимо получить данные пользователя, для доступа к которым требуется аутентификация. Вы можете сделать это, добавив новые параметры в ваш API интерфейс, например как в коде ниже:
@GET("user")Call<UserDetails> getUserDetails(@Header("Authorization") String credentials)
С помощью аннотации @Header(«Authorization») мы просим Retrofit добавить поле Authorization в заголовок запроса со значением, предоставленным в параметре credentials.
Для создания данных пользователя(credentials) мы можем использовать класс Credentials из библиотеки OkHttp, а именно его метод basic(String, String). Как видно из сигнатуры, этот метод получает логин с паролем и возвращает данные для проверки подлинности
Credentials.basic("ausername","apassword");
Если вы хотите использовать токен, просто вызовите метод getUserDetails(String)
3.2 Аутенфитикация с OkHttp интерцепторами.
Если у вас много вызовов, использующих аутентификацию, то стоит задуматься о использовании интерцепторов. Интерцептор используется для модификации каждого запроса перед его использованием и меняет его заголовок. Преимущество тут заключается в том, что вам не приходится добавлять аннотации @Header(«Authorization») в каждый вызов методов API.
Что бы добавить интерцептор, вы должны выполнить метод okhttp3.OkHttpClient.Builder.addInterceptor(Interceptor):
OkHttpClient okHttpClient = new OkHttpClient().newBuilder().addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder().header("Authorization", Credentials.basic("aUsername", "aPassword")); Request newRequest = builder.build(); return chain.proceed(newRequest); } }).build();
Созданный okHttpClient должен быть добавлен как ваш Retrofit клиент в методе retrofit2.Retrofit.Builder.client(OkHttpClient)
Retrofit retrofit = new Retrofit.Builder(). baseUrl("https://api.example.com"). client(okHttpClient). build();
Как и в примере с аутенфификацией с помощью аннотаций, вы можете пользоваться токеном везде, где используется класс Credentials
А теперь давайте создадим на android приложение, которое будет выгребать с сайта umori.li анекдоты с помощью Retrofit. Пример будет довольно примитивный и несет в себе лишь цель понять, как необходимо взаимодействовать с основными частями библиотеки.
Итак, помните я в начале упоминал про 3 сущности? Давайте поочередно их создавать. Первая — класс-модель. Воспользуемся сервисом http://www.jsonschema2pojo.org/ для получения класса AnekdotModel. Для этого идём по адресу http://www.umori.li/api, знакомимся с API, делаем запрос http://www.umori.li/api/get?site=anekdot.ru&name=new+anekdot&num=1 , и вставляем результат ответа уже в наш чудо-сервис. Выбираем JSON, стиль аннотаций Gson, включение сеттеров и геттеров. В результате получаем класс-модель, отражающий данные, которые будут приходить по сети.
package info.javaway.anekdot.model; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class AnekdotModel { @SerializedName("site") @Expose private String site; @SerializedName("name") @Expose private String name; @SerializedName("desc") @Expose private String desc; @SerializedName("link") @Expose private Object link; @SerializedName("elementPureHtml") @Expose private String elementPureHtml; public String getSite() { return site; } public void setSite(String site) { this.site = site; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public Object getLink() { return link; } public void setLink(Object link) { this.link = link; } public String getElementPureHtml() { return elementPureHtml; } public void setElementPureHtml(String elementPureHtml) { this.elementPureHtml = elementPureHtml; } }
С первым разобрались. Перед созданием второй сущности – интерфейса с описанием будущего API, давайте подключим библиотеку Retrofit в наше приложение. Для этого в Gradle необходимо добавить
compile 'com.squareup.retrofit2:retrofit:2.1.0'
Есть так же способы подключения библиотеки с помощью Maven или добавления Jar файла, с ними можете ознакомиться на сайте разработчиков библиотеки.
Приступим к созданию интерфейса с API. Он будет состоять из метода получения портянки с анекдотами.
package info.javaway.retrofittutorial.javaway.api; import java.util.List; import info.javaway.retrofittutorial.javaway.model.AnekdotModel; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface UmoriliApi { @GET("/api/get") Call<List<AnekdotModel>> getData(@Query("name") String resourceName, @Query("num") int count); }
Взглянув на аннотации, мы видим что будет использоваться метод GET, который к базовому URL добавит /api/get?name=”первый параметр”&num=”второй параметр”.
Третья сущность – класс, в котором будем инициализировать Retrofit. Назовем его Controller. Дёргая его за метод getApi мы получаем готовый экземпляр реализации нашего API, который описывали на втором шаге.
public class Controller { static final String BASE_URL = "http://www.umori.li/"; public static UmoriliApi getApi() { Gson gson = new GsonBuilder() .setLenient() .create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); UmoriliApi umoriliApi = retrofit.create(UmoriliApi.class); return umoriliApi; } }
Теперь нам осталось собрать все части в единое целое. Получаемыми данными в активности будем наполнять RecyclerView, часть Gradle с нужыми библиотеками в результате выглядит так
compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.android.support:recyclerview-v7:25.3.0'
Также не забываем добавить в файл манифеста разрешения на прогулки по интернету.
<uses-permission android:name="android.permission.INTERNET"/>
Создаем файлы разметки для активити и элемента списка RecyclerView:
activity_main.xml
<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="info.javaway.retrofittutorial.javaway.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: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:layout_below="@+id/postitem_post" android:layout_centerHorizontal="true" android:gravity="end" android:textAlignment="textEnd" /> </RelativeLayout>
Для работы с RecyclerView пишем адаптер. Надеюсь написание всех побочных вещей не вызывает у вас вопросов. Но если вы впервые видите RecyclerView, то можете прочитать о принципах работы с ней тут.
package info.javaway.retrofittutorial.javaway; 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; import info.javaway.retrofittutorial.R; import info.javaway.retrofittutorial.javaway.model.AnekdotModel; public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.ViewHolder> { private List<AnekdotModel> posts; public PostsAdapter(List<AnekdotModel> 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) { AnekdotModel 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, где вся магия обретает жизнь:
package info.javaway.retrofittutorial.javaway; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.widget.Toast; import java.util.ArrayList; import java.util.List; import info.javaway.retrofittutorial.R; import info.javaway.retrofittutorial.javaway.api.UmoriliApi; import info.javaway.retrofittutorial.javaway.controller.Controller; import info.javaway.retrofittutorial.javaway.model.AnekdotModel; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; public class MainActivity extends AppCompatActivity { private static UmoriliApi umoriliApi; RecyclerView recyclerView; List<AnekdotModel> posts; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); umoriliApi = Controller.getApi(); 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 = umoriliApi.getData("bash", 50).execute(); } catch (IOException e) { e.printStackTrace(); }*/ umoriliApi.getData("new anekdot", 50).enqueue(new Callback<List<AnekdotModel>>() { @Override public void onResponse(Call<List<AnekdotModel>> call, Response<List<AnekdotModel>> response) { posts.addAll(response.body()); recyclerView.getAdapter().notifyDataSetChanged(); } @Override public void onFailure(Call<List<AnekdotModel>> call, Throwable t) { Toast.makeText(MainActivity.this, "An error occurred during networking", Toast.LENGTH_SHORT).show(); } }); } }
Закомментирован участок где демонстрируется синхронный вариант вызова. Но вы же знаете, что в главном потоке не кошерно вызывать методы, которые могут подвесить UI, так что пользуйтесь им в бэкграунде по необходимости.
Подведя итог можно выделить следующие фазы написания приложения:
А) Подготовительная. Добавление зависимостей в Gradle, разрешений на работу сетью.
Б) Создание модели данных, так называемого POJO объекта(англ. Plain Old Java Object — «старый добрый Java-объект»). Выполняем запрос к ресурсу, ответ вставляем в один из сервисов(например http://www.jsonschema2pojo.org/) и забираем готовую модель. Можете и вручную описать класс, это уже на ваше усмотрение.
В) Реализация инициализации retrofit’а. Тут вы сами вольны принять архитектурныое решение и разместить инициализацию где вашей душе угодно. Можете сделать это в отдельном контроллере, как в коде выше, в централизованном месте(например в классе App, кунаследовавшись от Application), или же выполнить реализацию в активити, непосредственно перед использованием библиотеки.
Г) Вызов методов API из реализации модели, которую построит при инициализации Retrofit.Builder и обработка результатов их работы. Размещение данных для показа пользователю, запись в базу данных, построение графика и т.д.
Согласитесь, Retrofit отлично справляется с задачей сокрытия низкоуровневых вызовов, оставляя нам методы API и обработку их обратных вызовов. Надеюсь, что вы сможете избежать массы костылей при работе с сетью, используя эту чудесную библиотеку!
P.s. код на гитхабе
У меня получается следующая ошибка:
После запроса к серверу — уходит в OnFailure с ошибкой:
OnFailure: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 1 path $
Подскажите, пожалуйста, в чем может быть проблема?
Заранее спасибо за ответ.
Добрый день. Скажите, пожалуйста, вы решили проблему с этой ошибкой? просто у меня та же ошибка и не могу найти решение. Спасибо.
В общем я разобрался почему такую ошибку выдает, по такому адресу сервер возвращает «This page requires that your browser supports frames.»
Нужно в BASE_URL вместо «www.umori.li» поставить
«umorili.herokuapp.com»
Мне такую ошибку не выводило, а просто выводило ответный json, который на сколько я понял, не совсем корректный. Но внешне они идентичны. Понял это по тому, как парсит этот json встроенный в хром плагин. Так вот, с сайта umiori.li он не парсился, в то время как с umorili.herokuapp.com все хорошо парсится и код работает. Непонятно =)
А куда девался вот этот кусок адреса
?site=anekdot.ru ???
Сори, код со своего рабочего приложения Улыбаш скопировал. Сейчас поправим=)
Спасибо за замечательный туториал, но возникла одна проблемка. Я новичок в ретрофите и вообще в роботе с веб — сервисами и джсоном. У меня возникло исключение- IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 1 path $. Подскажите, пожалуйста, что с этим поделать.
в Call он от вас ждет массив json а вы ему даете объект, о чем он Вам собственно и сообщает…
А как, это, собственно, исправить? я прост новичок в ретрофите, можете подсказать как решить подобную проблему?
Посмотрите коммент и мой ответ под ним
http://javaway.info/ispolzovanie-retrofit-2-v-prilozheniyah-android/#comment-4108