1. Retrofit
1.1. Что такое Retrofit
Retrofit — это REST клиент для Java и Android. Он позволяет легко получить и загрузить JSON (или другие структурированные данные) через веб-сервис на основе REST. В Retrofit вы настраиваете, какой конвертер используется для сериализации данных. Обычно для JSON используется GSon, но вы можете добавлять собственные конвертеры для обработки XML или других протоколов. В Retrofit используется библиотека OkHttp для HTTP-запросов.
Вы можете создавать объекты Java на основе JSON, используя следующий инструмент: www.jsonschema2pojo.org Это может быть полезно для создания сложных структур данных Java из существующего JSON.
1.2. Использование Retrofit
Для работы с Retrofit вам понадобятся следующие три класса:
- Model класс, который используется как модель JSON
- Интерфейсы, которые определяют возможные HTTP операции
- Класс Retrofit.Builder — экземпляр, который использует интерфейс и API Builder, чтобы задать определение конечной точки URL для операций HTTP
Каждый метод интерфейса представляет собой один из возможных вызовов API. Он должен иметь HTTP аннотацию (GET, POST и т. д.), чтобы указать тип запроса и относительный URL. Возвращаемое значение завершает ответ в Call-объекте с типом ожидаемого результата.
@GET("users")
Call<List<User>> getUsers();
Можно использовать замещающие блоки и параметры запроса для настройки URL-адреса. Замещающий блок добавляется к относительному URL-адресу с помощью {}. С помощью аннотации @ Path для параметра метода значение этого параметра привязывается к конкретному замещающему блоку.
@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. Предварительные требования
В следующих примерах используется Eclipse IDE с системой сборки Gradle.
Это упражнение предполагает, что вы знакомы с Gradle и использованием Gradle с Eclipse.
Другие среды разработки, такие как Visual Studio Code или IntelliJ, позволяют сделать то же самое, так что вы можете использовать свой любимый инструмент.
3. Упражнение: Первый Retrofit клиент
В этом упражнении вы создадите автономный REST клиент. Ответы генерируются Mock-сервером.
3.1. Создание и настройка проекта
Создайте новый проект Gradle, с именем com.vogella.retrofitgerrit. Добавьте новый пакет в src/main/java с именем com.vogella.retrofitgerrit.
Добавьте следующие зависимости в файл build.gradle.
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
// Junit
testImplementation("org.junit.jupiter:junit-jupiter-api:5.0.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0")
// to run JUnit 3/4 tests:
testImplementation("junit:junit:4.12")
testRuntime("org.junit.vintage:junit-vintage-engine:4.12.0")
3.2. Определите API и Retrofit адаптер
В JSON ответе от Gerrit нас интересует только вопрос об изменениях. Поэтому создайте следующий класс данных в ранее добавленном пакете по умолчанию.
package com.vogella.java.retrofitgerrit;
public class Change {
String subject;
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}
Определите REST API для Retrofit через следующий интерфейс.
package com.vogella.java.retrofitgerrit;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface GerritAPI {
@GET("changes/")
Call<List<Change>> loadChanges(@Query("q") String status);
}
Создайте следующий класс контроллера. Этот класс создает Retrofit клиент, вызывает Gerrit API и обрабатывает результат (выводит результат вызова в консоль).
package com.vogella.java.retrofitgerrit;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class Controller implements Callback<List<Change>> {
static final String BASE_URL = "https://git.eclipse.org/r/";
public void start() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
GerritAPI gerritAPI = retrofit.create(GerritAPI.class);
Call<List<Change>> call = gerritAPI.loadChanges("status:open");
call.enqueue(this);
}
@Override
public void onResponse(Call<List<Change>> call, Response<List<Change>> response) {
if(response.isSuccessful()) {
List<Change> changesList = response.body();
changesList.forEach(change -> System.out.println(change.subject));
} else {
System.out.println(response.errorBody());
}
}
@Override
public void onFailure(Call<List<Change>> call, Throwable t) {
t.printStackTrace();
}
}
Создайте класс с main-методом для запуска контроллера.
package com.vogella.java.retrofitgerrit;
public class Main {
public static void main(String[] args) {
Controller controller = new Controller();
controller.start();
}
}
4. Retrofit конвертеры и адаптеры
4.1. Retrofit конвертеры
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.
4.2. Retrofit Адаптеры
Retrofit также может быть расширен адаптерами для взаимодействия с другими библиотеками, такими как RxJava 2.x, Java 8 и Guava.
Обзор доступных адаптеров можно найти на Github square/retrofit/retrofit-adapters/.
Например, адаптер RxJava 2.x можно подключить, используя Gradle:
compile 'com.squareup.retrofit2:adapter-rxjava2:latest.version'
или используя Apache 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();
5. Retrofit аутентификация
Retrofit поддерживает вызовы API, требующие аутентификации. Аутентификацию можно выполнить, используя имя пользователя и пароль (аутентификация Http Basic) или API токен.
Существует два способа управления аутентификацией. Первый метод — управлять заголовком запроса с помощью аннотаций. Другой способ — использовать для этого OkHttp перехватчик.
5.1. Аутентификация с аннотациями
Предположим, что вы хотите запросить информацию о пользователе, для которой требуется аутентификация. Вы можете сделать это, добавив новый параметр в определение API, например:
@GET("user")
Call<UserDetails> getUserDetails(@Header("Authorization") String credentials)
С помощью аннотации @ Header(«Authorization») вы говорите Retrofit добавить заголовок Authorization в запрос со значением, которое вы передаете.
Чтобы генерировать учетные данные для Basic authentication, вы можете использовать класс OkHttps Credentials с его базовым (String, String) методом. Метод принимает имя пользователя и пароль и возвращает учетные данные аутентификации для Http Basic схемы.
Credentials.basic("ausername","apassword");
Если вы хотите использовать API токен и не использовать Basic схему, просто вызовите метод getUserDetails(String) с вашим токеном.
5.2. Аутентификация с помощью OkHttp перехватчиков.
Метод выше добавляет учетные данные, только если вы запрашиваете данные пользователя. Если у вас больше вызовов, требующих аутентификации, для этого вы можете использовать перехватчик. Перехватчик используется для изменения каждого запроса до его выполнения и устанавливает заголовок запроса. Преимущество состоит в том, что вам не нужно добавлять @ Header(«Authorization») к каждому определению метода API.
Чтобы добавить перехватчик, вы должны использовать метод okhttp3.OkHttpClient.Builder.addInterceptor(Interceptor) в OkHttp Builder.
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();
Созданный OkHttp клиент должен быть добавлен в ваш Retrofit клиент с помощью метода retrofit2.Retrofit.Builder.client(OkHttpClient).
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.build();
Как вы заметили, здесь используется класс Credentials для Basic авторизации.
Опять же, если вы хотите использовать токен API, просто используйте вместо этого токен.
6. Упражнение: использование Retrofit для запроса к Gerrit в Java
В следующем разделе описывается, как создать минимальное Java приложение, которое использует Retrofit для получения объектов открытых изменений из Gerrit API. Результаты печатаются в консоли.
6.1. Создание и настройка проекта
Это упражнение предполагает, что вы знакомы с Gradle и Buildship для Eclipse.
Создайте новый проект Gradle с именем com.vogella.java.retrofitgerrit. Добавьте новый пакет в src/main/java с именем com.vogella.java.retrofitgerrit.
Добавьте следующие зависимости в файл build.gradle.
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
// Junit
testImplementation("org.junit.jupiter:junit-jupiter-api:5.0.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0")
// to run JUnit 3/4 tests:
testImplementation("junit:junit:4.12")
testRuntime("org.junit.vintage:junit-vintage-engine:4.12.0")
6.2. Определите API и Retrofit адаптер
В JSON ответе от Gerrit нас интересует только вопрос об изменениях. Поэтому создайте следующий класс данных в ранее добавленном пакете по умолчанию.
package com.vogella.java.retrofitgerrit;
public class Change {
String subject;
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}
Определите REST API для Retrofit с помощью следующего интерфейса.
package com.vogella.java.retrofitgerrit;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface GerritAPI {
@GET("changes/")
Call<List<Change>> loadChanges(@Query("q") String status);
}
Создайте следующий класс контроллера. Этот класс создает Retrofit клиент, вызывает Gerrit API и обрабатывает результат (выводит результат вызова в консоль).
package com.vogella.java.retrofitgerrit;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class Controller implements Callback<List<Change>> {
static final String BASE_URL = "https://git.eclipse.org/r/";
public void start() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
GerritAPI gerritAPI = retrofit.create(GerritAPI.class);
Call<List<Change>> call = gerritAPI.loadChanges("status:open");
call.enqueue(this);
}
@Override
public void onResponse(Call<List<Change>> call, Response<List<Change>> response) {
if(response.isSuccessful()) {
List<Change> changesList = response.body();
changesList.forEach(change -> System.out.println(change.subject));
} else {
System.out.println(response.errorBody());
}
}
@Override
public void onFailure(Call<List<Change>> call, Throwable t) {
t.printStackTrace();
}
}
Создайте класс с main-методом для запуска контроллера.
package com.vogella.java.retrofitgerrit;
public class Main {
public static void main(String[] args) {
Controller controller = new Controller();
controller.start();
}
}
7. Упражнение: использование Retrofit для конвертирования XML-ответа от RSS-канала
В этом разделе описывается использование Retrofit для преобразования XML-ответа с помощью SimpleXMLConverter.
Создается минимальное приложение Java, которое запрашивает RSS-канал Vogella (http://vogella.com/article.rss) и печатает название канала, названия и ссылки на статьи.
7.1. Создание и настройка проекта
Это упражнение предполагает, что вы знакомы с Gradle и Buildship для Eclipse.
Создайте новый проект Gradle с именем com.vogella.java.retrofitxml. Добавьте новый пакет в src/main/java с именем com.vogella.java.retrofitxml.
Добавьте следующие зависимости в файл build.gradle.
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-simplexml:2.1.0'
7.2. Определяем XML представление
Новостная RSS лента выглядит следующим образом:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Eclipse and Android Information</title>
<link>http://www.vogella.com</link>
<description>Eclipse and Android Information</description>
<language>en</language>
<copyright>Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Germany (CC BY-NC-SA 3.0)</copyright>
<pubDate>Tue, 03 May 2016 11:46:11 +0200</pubDate>
<item>
<title>Android user interface testing with Espresso - Tutorial</title>
<description> This tutorial describes how to test Android applications with the Android Espresso testing framework.</description>
<link>http://www.vogella.com/tutorials/AndroidTestingEspresso/article.html</link>
<author>lars.vogel@vogella.com (Lars Vogel)</author>
<guid>http://www.vogella.com/tutorials/AndroidTestingEspresso/article.html</guid>
</item>
<item>
<title>Using the Gradle build system in the Eclipse IDE - Tutorial</title>
<description>This article describes how to use the Gradle tooling in Eclipse</description>
<link>http://www.vogella.com/tutorials/EclipseGradle/article.html</link>
<author>lars.vogel@vogella.com (Lars Vogel)</author>
<guid>http://www.vogella.com/tutorials/EclipseGradle/article.html</guid>
</item>
<item>
<title>Unit tests with Mockito - Tutorial</title>
<description>This tutorial explains testing with the Mockito framework for writting software tests.</description>
<link>http://www.vogella.com/tutorials/Mockito/article.html</link>
<author>lars.vogel@vogella.com (Lars Vogel)</author>
<guid>http://www.vogella.com/tutorials/Mockito/article.html</guid>
</item>
</channel>
</rss>
Кроме XML заголовка этот файл состоит из различных элементов XML. Rss-элемент содержит элемент канала, в котором содержатся другие элементы (например, title, description, pubDate) и несколько item-элементов (статей).
Создайте следующие два класса данных: RSSFeed и Article.
package com.vogella.java.retrofitxml;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
@Root(name = "item", strict = false)
public class Article {
@Element(name = "title")
private String title;
@Element(name = "link")
private String link;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
}
package com.vogella.java.retrofitxml;
import java.util.List;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Path;
import org.simpleframework.xml.Root;
@Root(name="rss", strict=false)
public class RSSFeed {
@Element(name="title")
@Path("channel")
private String channelTitle;
@ElementList(name="item", inline=true)
@Path("channel")
private List<Article> articleList;
public String getChannelTitle() {
return channelTitle;
}
public void setChannelTitle(String channelTitle) {
this.channelTitle = channelTitle;
}
public List<Article> getArticleList() {
return articleList;
}
public void setArticleList(List<Article> articleList) {
this.articleList = articleList;
}
}
Класс Article представляет одну статью и сохраняет только название и ссылку на статью. Это единственные поля, которые нас интересуют.
Аннотация @ Root помечает класс как подлежащий (де)сериализации. При желании вы можете указать имя в @ Root аннотации, которая соответствует имени XML элемента. Если имя не указано, имя класса используется как имя XML элемента. Поскольку имя класса (RSSFeed) отличается от имени элемента XML (rss), нам нужно указать имя.
Когда в strict параметр установлено значение false, строгий парсинг отключен. Это говорит парсеру не прерываться и не генерировать исключение, если найден XML элемент или атрибут, для которого не представлено сопоставление. Поскольку rss-элемент имеет атрибут version, для которого нет соответствующего поля, приложение будет генерировать ошибку, если strict параметр не установлен как false.
С помощью аннотации @ Element представляется XML-элемент. При необходимости можно указать имя XML элемента, представленного этим полем. Если имя не указано, используется имя поля.
Поле articleList аннотируется с помощью @ ElementList. Это показывает, что это поле используется для хранения коллекции (в нашем случае: List ) XML элементов с тем же именем. Когда inline установлено значение true, это значит, что элементы коллекции перечислены один за другим сразу внутри заданного элемента и не имеют промежуточного родительского элемента.
С помощью аннотации @ Path можно указать путь к элементу XML внутри дерева XML. Это полезно, если вы не хотите моделировать полное дерево XML с объектами Java. Для названия канала и нескольких item-элементов мы можем напрямую указывать на конкретные элементы в channel-элементе.
7.3. Определение API и Retrofit адаптера
Определите REST API для Retrofit через следующий интерфейс.
package com.vogella.java.retrofitxml;
import retrofit2.Call;
import retrofit2.http.GET;
public interface VogellaAPI {
@GET("article.rss")
Call<RSSFeed> loadRSSFeed();
}
Создайте следующий класс контроллера. Этот класс создает Retrofit клиент, вызывает Vogella API и обрабатывает результат.
package com.vogella.java.retrofitxml;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.simplexml.SimpleXmlConverterFactory;
public class Controller implements Callback<RSSFeed> {
static final String BASE_URL = "http://vogella.com/";
public void start() {
Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(SimpleXmlConverterFactory.create()).build();
VogellaAPI vogellaAPI = retrofit.create(VogellaAPI.class);
Call<RSSFeed> call = vogellaAPI.loadRSSFeed();
call.enqueue(this);
}
@Override
public void onResponse(Call<RSSFeed> call, Response<RSSFeed> response) {
if (response.isSuccessful()) {
RSSFeed rss = response.body();
System.out.println("Channel title: " + rss.getChannelTitle());
rss.getArticleList().forEach(
article -> System.out.println("Title: " + article.getTitle() + " Link: " + article.getLink()));
} else {
System.out.println(response.errorBody());
}
}
@Override
public void onFailure(Call<RSSFeed> call, Throwable t) {
t.printStackTrace();
}
}
Последний шаг — создать класс с main-методом для запуска контроллера.
package com.vogella.java.retrofitxml;
public class Application {
public static void main(String[] args) {
Controller ctrl = new Controller();
ctrl.start();
}
}
8. Упражнение: Создание приложения для запроса к StackOverflow
StackOverflow — популярный сайт для вопросов связанных с программированиес. Он также предоставляет REST API, хорошо документированный на странице Stackoverflow API.
В этом упражнении вы будете использовать REST Retrofit библиотеку. Вы будете ее использовать для запроса к StackOverflow вопросов по тегу и их ответов.
В нашем примере мы используем следующий URL-адрес запроса. Откройте этот URL в браузере и посмотрите на ответ.
https://api.stackexchange.com/2.2/search?order=desc&sort=votes&tagged=android&site=stackoverflow
8.1. Создание и настройка проекта
Создайте приложение для Android, с названием com.vogella.android.stackoverflow. Используйте com.vogella.android.stackoverflow как имя пакета верхнего уровня.
Добавьте следующие зависимости в файл build.gradle.
compile "com.android.support:recyclerview-v7:25.3.1"
compile 'com.google.code.gson:gson:2.8.1'
8.2. Создание модели данных
Нас интересуют вопросы и ответы от Stackoverflow. Для этой цели создайте следующие два класса данных.
package com.vogella.android.stackoverflow;
import com.google.gson.annotations.SerializedName;
public class Question {
public String title;
public String body;
@SerializedName("question_id")
public String questionId;
@Override
public String toString() {
return(title);
}
}
package com.vogella.android.stackoverflow;
import com.google.gson.annotations.SerializedName;
public class Answer {
@SerializedName("answer_id")
public int answerId;
@SerializedName("is_accepted")
public boolean accepted;
public int score;
@Override
public String toString() {
return answerId + " - Score: " + score + " - Accepted: " + (accepted ? "Yes" : "No");
}
}
8.3. Создание activity и layout
Задайте activity_main.xml для вашей activity.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_margin="8dp"
android:orientation="vertical"
tools:context="com.vogella.android.stackoverflow.MainActivity">
<Spinner
android:id="@+id/questions_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/authenticate_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="Authenticate" />
</LinearLayout>
Добавьте в свой проект класс recycler view адаптера, с именем RecyclerViewAdapter.
Одна из возможных реализаций выглядит следующим образом.
package com.vogella.android.stackoverflow;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private List<Answer> data;
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView text;
public ViewHolder(View v) {
super(v);
text = (TextView) v.findViewById(android.R.id.text1);
}
}
public RecyclerViewAdapter(List<Answer> data) {
this.data = data;
}
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
v = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_selectable_list_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, int position) {
Answer answer = ((Answer) data.get(position));
holder.text.setText(answer.toString());
holder.itemView.setTag(answer.answerId);
}
@Override
public int getItemCount() {
return data.size();
}
}
Измените класс MainActivity таким образом:
package com.vogella.android.stackoverflow;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends Activity implements View.OnClickListener {
private String token;
private Button authenticateButton;
private Spinner questionsSpinner;
private RecyclerView recyclerView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
questionsSpinner = (Spinner) findViewById(R.id.questions_spinner);
questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "Spinner item selected", Toast.LENGTH_LONG).show();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
authenticateButton = (Button) findViewById(R.id.authenticate_button);
recyclerView = (RecyclerView) findViewById(R.id.list);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
}
@Override
protected void onResume() {
super.onResume();
if (token != null) {
authenticateButton.setEnabled(false);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case android.R.id.text1:
if (token != null) {
// TODO
} else {
Toast.makeText(this, "You need to authenticate first", Toast.LENGTH_LONG).show();
}
break;
case R.id.authenticate_button:
// TODO
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && requestCode == 1) {
token = data.getStringExtra("token");
}
}
}
8.4. Использование поддельного поставщика данных
Создайте поддельный поставщик данных и заполните spinner фальшивыми вопросами и recyclerview фальшивыми ответами (после изменения выбора в spinner).
package com.vogella.android.stackoverflow;
import java.util.ArrayList;
import java.util.List;
public class FakeDataProvider {
public static List<Question> getQuestions(){
List<Question> questions = new ArrayList<>();
for (int i = 0; i<10; i++) {
Question question = new Question();
question.questionId = String.valueOf(i);
question.title = String.valueOf(i);
question.body = String.valueOf(i) + "Body";
questions.add(question);
}
return questions;
}
public static List<Answer> getAnswers(){
List<Answer> answers = new ArrayList<>();
for (int i = 0; i<10; i++) {
Answer answer = new Answer();
answer.answerId = i;
answer.accepted = false;
answer.score = i;
answers.add(answer);
}
return answers;
}
}
Теперь настроим spinner и recyclerview для использования этих поддельных данных.
package com.vogella.android.stackoverflow;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;
import java.util.List;
public class MainActivity extends Activity implements View.OnClickListener {
private String token;
private Button authenticateButton;
private Spinner questionsSpinner;
private RecyclerView recyclerView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
questionsSpinner = (Spinner) findViewById(R.id.questions_spinner);
questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "Spinner item selected", Toast.LENGTH_LONG).show();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
List<Question> questions = FakeDataProvider.getQuestions();
ArrayAdapter<Question> arrayAdapter = new ArrayAdapter<Question>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, questions);
questionsSpinner.setAdapter(arrayAdapter);
authenticateButton = (Button) findViewById(R.id.authenticate_button);
recyclerView = (RecyclerView) findViewById(R.id.list);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
List<Answer> answers = FakeDataProvider.getAnswers();
RecyclerViewAdapter adapter = new RecyclerViewAdapter(answers);
recyclerView.setAdapter(adapter);
}
@Override
protected void onResume() {
super.onResume();
if (token != null) {
authenticateButton.setEnabled(false);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case android.R.id.text1:
if (token != null) {
// TODO
} else {
Toast.makeText(this, "You need to authenticate first", Toast.LENGTH_LONG).show();
}
break;
case R.id.authenticate_button:
// TODO
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && requestCode == 1) {
token = data.getStringExtra("token");
}
}
}
8.5. Добавление Gradle зависимостей и разрешений
Добавьте следующие зависимости в build.gradle файл.
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
Добавьте разрешение на доступ к Интернету в манифест.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vogella.android.stackoverflow">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
8.6. Определение API и Retrofit адаптера
Stackoverflow API оборачивает ответы или вопросы в JSON объект с именем items. Чтобы обработать это, создайте следующий класс данных с именем ListWrapper. Это необходимо для того, чтобы обработать обертку элементов Stackoverflow. Этот класс принимает параметр типа и просто упаковывает список объектов этого типа.
package com.vogella.android.stackoverflow;
import java.util.List;
public class ListWrapper<T> {
List<T> items;
}
Определите REST API для Retrofit через следующий интерфейс.
package com.vogella.android.stackoverflow;
import java.util.List;
import okhttp3.ResponseBody;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.Call;
public interface StackOverflowAPI {
String BASE_URL = "https://api.stackexchange.com/";
@GET("/2.2/questions?order=desc&sort=votes&site=stackoverflow&tagged=android&filter=withbody")
Call<ListWrapper<Question>> getQuestions();
@GET("/2.2/questions/{id}/answers?order=desc&sort=votes&site=stackoverflow")
Call<ListWrapper<Answer>> getAnswersForQuestion(@Path("id") String questionId);
}
8.7. Установка activity
Измените код MainActivity следующим образом.
package com.vogella.android.stackoverflow;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.ArrayList;
import java.util.List;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends Activity implements View.OnClickListener {
private StackOverflowAPI stackoverflowAPI;
private String token;
private Button authenticateButton;
private Spinner questionsSpinner;
private RecyclerView recyclerView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
questionsSpinner = (Spinner) findViewById(R.id.questions_spinner);
questionsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Question question = (Question) parent.getAdapter().getItem(position);
stackoverflowAPI.getAnswersForQuestion(question.questionId).enqueue(answersCallback);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
authenticateButton = (Button) findViewById(R.id.authenticate_button);
recyclerView = (RecyclerView) findViewById(R.id.list);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
createStackoverflowAPI();
stackoverflowAPI.getQuestions().enqueue(questionsCallback);
}
@Override
protected void onResume() {
super.onResume();
if (token != null) {
authenticateButton.setEnabled(false);
}
}
private void createStackoverflowAPI() {
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(StackOverflowAPI.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
stackoverflowAPI = retrofit.create(StackOverflowAPI.class);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case android.R.id.text1:
if (token != null) {
//TODO
} else {
Toast.makeText(this, "You need to authenticate first", Toast.LENGTH_LONG).show();
}
break;
case R.id.authenticate_button:
// TODO
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && requestCode == 1) {
token = data.getStringExtra("token");
}
}
Callback<ListWrapper<Question>> questionsCallback = new Callback<ListWrapper<Question>>() {
@Override
public void onResponse(Call<ListWrapper<Question>> call, Response<ListWrapper<Question>> response) {
if (response.isSuccessful()) {
ListWrapper<Question> questions = response.body();
ArrayAdapter<Question> arrayAdapter = new ArrayAdapter<Question>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, questions.items);
questionsSpinner.setAdapter(arrayAdapter);
} else {
Log.d("QuestionsCallback", "Code: " + response.code() + " Message: " + response.message());
}
}
@Override
public void onFailure(Call<ListWrapper<Question>> call, Throwable t) {
t.printStackTrace();
}
};
Callback<ListWrapper<Answer>> answersCallback = new Callback<ListWrapper<Answer>>() {
@Override
public void onResponse(Call<ListWrapper<Answer>> call, Response<ListWrapper<Answer>> response) {
if (response.isSuccessful()) {
List<Answer> data = new ArrayList<>();
data.addAll(response.body().items);
recyclerView.setAdapter(new RecyclerViewAdapter(data));
} else {
Log.d("QuestionsCallback", "Code: " + response.code() + " Message: " + response.message());
}
}
@Override
public void onFailure(Call<ListWrapper<Answer>> call, Throwable t) {
t.printStackTrace();
}
};
Callback<ResponseBody> upvoteCallback = new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
Toast.makeText(MainActivity.this, "Upvote successful", Toast.LENGTH_LONG).show();
} else {
Log.d("QuestionsCallback", "Code: " + response.code() + " Message: " + response.message());
Toast.makeText(MainActivity.this, "You already upvoted this answer", Toast.LENGTH_LONG).show();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
t.printStackTrace();
}
};
}
8.8. Необязательно: получение изображения профиля пользователя
Измените layout строки в recycler view, чтобы отобразить также изображение профиля пользователя. Расширьте свою модель данных, чтобы получать изображение профиля пользователя, который ответил на вопрос. Добавьте ImageView в layout строки и используйте библиотеку Glide для загрузки изображения.
8.9. Необязательно: используйте разные layout’ы для четных и нечетных строк
Измените реализацию адаптера, чтобы использовать разные макеты для четных и нечетных строк.
Это требует создания различных layout на основе типа данных. Используйте getItemViewType() в адаптере.
8.10. Необязательно: Обработка ошибки сети
Если у вас произошел сбой в сети, покажите кнопку повторного запроса вместо основного пользовательского интерфейса.
9. Упражнение: Использование Retrofit для доступа к GitHub API в Android
В этом упражнении описывается, как перечислить все GitHub репозитории для пользователя в приложении для Android с помощью Retrofit. Вы можете выбрать репозиторий из раскрывающегося списка и указать обсуждения (issues), относящиеся к пользователю для выбранного репозитория.
Затем вы можете выбрать обсуждение из дополнительного раскрывающегося поля и опубликовать комментарий к нему. DialogFragment будет использоваться для ввода учетных данных для аутентификации.
Убедитесь, что у вас есть учетная запись Github, поскольку это необходимо для этого упражнения. Поскольку Retrofit будет использоваться вместе с RxJava2 во время этого упражнения, обратите внимание также на RxJava2 Tutorial.
9.1. Настройка проекта
Создайте Android приложение с именем Retrofit Github. Используйте com.vogella.android.retrofitgithub как имя пакета верхнего уровня и используйте пустой шаблон. Убедитесь, что проставлен флаг «Backwards Compatibility» (Обратная совместимость).
Чтобы использовать Retrofit и RxJava2 CallAdapter, добавьте следующие зависимости в файл build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
Добавьте разрешение на доступ к Интернету в манифест.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vogella.android.retrofitgithub">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.vogella.android.retrofitgithub.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
9.2. Определение API
Создайте следующие два класса данных: GithubIssue и GithubRepo.
package com.vogella.android.retrofitgithub;
import com.google.gson.annotations.SerializedName;
public class GithubIssue {
String id;
String title;
String comments_url;
@SerializedName("body")
String comment;
@Override
public String toString() {
return id + " - " + title;
}
}
package com.vogella.android.retrofitgithub;
public class GithubRepo {
String name;
String owner;
String url;
@Override
public String toString() {
return(name + " " + url);
}
}
Из информации о репозиториях только имя и URL-адрес репозитория будут отображены в раскрывающемся списке. Также добавляем владельца (owner) в класс данных, так как имя владельца необходимо для того, чтобы позже запрашивать обсуждения.
Мы показываем только id и заголовок обсуждения в раскрывающемся поле, поэтому создаем поле для каждого из них. Кроме того, ответ от Github содержит URL-адрес для публикации комментария, который сохраняется в поле comments_url. Чтобы позже опубликовать новый комментарий к Github API, добавляем поле с именем comment. Github API указывает, что содержимое комментария должно быть привязано к полю с именем body в запросе JSON. Поскольку Retrofit (де)сериализует все поля на основе их имени, и поскольку мы не хотим использовать тело в качестве имени поля в нашем классе GithubIssue, мы используем аннотацию @SerializedName. С помощью этой аннотации мы можем изменить имя, с которым поле (де)сериализуется в JSON.
К сожалению, класса GithubRepo недостаточно, чтобы запросить всю необходимую информацию о репозитории. Как вы видите здесь, владелец репозитория является отдельным JSON объектом в ответе репозитория, и поэтому обычно ему нужен соответствующий Java-класс для (де)сериализации. К счастью, Retrofit дает возможность добавить собственный типизированный JSONDeserializer для управления десериализацией определенного типа. Каждый раз, когда объект определенного типа должен быть десериализован, используется этот пользовательский десериализатор.
Для этого добавьте следующий класс GithubRepoDeserialzer.
package com.vogella.android.retrofitgithub;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
public class GithubRepoDeserializer implements JsonDeserializer<GithubRepo> {
@Override
public GithubRepo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
GithubRepo githubRepo = new GithubRepo();
JsonObject repoJsonObject = json.getAsJsonObject();
githubRepo.name = repoJsonObject.get("name").getAsString();
githubRepo.url = repoJsonObject.get("url").getAsString();
JsonElement ownerJsonElement = repoJsonObject.get("owner");
JsonObject ownerJsonObject = ownerJsonElement.getAsJsonObject();
githubRepo.owner = ownerJsonObject.get("login").getAsString();
return githubRepo;
}
}
Определите REST API для Retrofit через следующий интерфейс:
package com.vogella.android.retrofitgithub;
import java.util.List;
import io.reactivex.Single;
import okhttp3.ResponseBody;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Url;
public interface GithubAPI {
String ENDPOINT = "https://api.github.com";
@GET("user/repos?per_page=100")
Single<List<GithubRepo>> getRepos();
@GET("/repos/{owner}/{repo}/issues")
Single<List<GithubIssue>> getIssues(@Path("owner") String owner, @Path("repo") String repository);
@POST
Single<ResponseBody> postComment(@Url String url, @Body GithubIssue issue);
}
У вас может возникнуть вопрос об аннотации @ Url. С помощью этой аннотации мы можем указать URL для этого запроса. Это позволяет нам изменять URL для каждого запроса динамически. Нам нужно это для поля comments_url класса GithubIssue.
Аннотации @ Path связывают значение параметра с соответствующей переменной (фигурные скобки) в URL-адресе запроса. Это необходимо для указания владельца и имени репозитория, для которого должны быть запрошены обсуждения.
9.3. Диалоговое окно «Создание учетных данных»
Чтобы предоставить пользователю возможность хранить свои учетные данные в приложении, используется DialogFragment. Поэтому создайте следующий класс с именем CredentialsDialog, а также добавьте layout файл с именем dialog_credentials.xml в папку layout ресурсов.
Результат должен выглядеть примерно так, как показано на следующем скриншоте.
<?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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:text="Fill you credentials here" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:text="Username:" />
<EditText
android:id="@+id/username_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:text="Password" />
<EditText
android:id="@+id/password_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:inputType="textPassword" />
</LinearLayout>
</LinearLayout>
package com.vogella.android.retrofitgithub;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.EditText;
import okhttp3.Credentials;
public class CredentialsDialog extends DialogFragment {
EditText usernameEditText;
EditText passwordEditText;
ICredentialsDialogListener listener;
public interface ICredentialsDialogListener {
void onDialogPositiveClick(String username, String password);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getActivity() instanceof ICredentialsDialogListener) {
listener = (ICredentialsDialogListener) getActivity();
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_credentials, null);
usernameEditText = (EditText) view.findViewById(R.id.username_edittext);
passwordEditText = (EditText) view.findViewById(R.id.password_edittext);
usernameEditText.setText(getArguments().getString("username"));
passwordEditText.setText(getArguments().getString("password"));
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setView(view)
.setTitle("Credentials")
.setNegativeButton("Cancel", null)
.setPositiveButton("Continue", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener != null) {
listener.onDialogPositiveClick(usernameEditText.getText().toString(), passwordEditText.getText().toString());
}
}
});
return builder.create();
}
}
9.4. Создание Activity
Измените activity_main.xml следующим образом.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context="com.vogella.android.retrofitgithub.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/my_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Spinner
android:id="@+id/repositories_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Spinner
android:id="@+id/issues_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/repositories_spinner" />
<EditText
android:id="@+id/comment_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/issues_spinner"
android:enabled="false"
android:hint="Your comment"
android:imeOptions="actionDone"
android:inputType="text"
android:maxLines="1" />
<Button
android:id="@+id/loadRepos_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:enabled="false"
android:gravity="center"
android:onClick="onClick"
android:text="Load user repositories" />
<Button
android:id="@+id/send_comment_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/loadRepos_button"
android:enabled="false"
android:onClick="onClick"
android:text="Send comment" />
</RelativeLayout>
</LinearLayout>
Две кнопки (для загрузки репозиториев и отправки комментария), два Spinner (раскрывающееся поле для отображения репозиториев и обсуждений) и EditText (для написания комментариев). Для запуска CredentialsDialog используется меню на панели инструментов Android. Чтобы создать его, добавьте xml файл меню с именем menu_main.xml в папку ресурсов меню (создайте папку, если она не существует).
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_credentials"
android:title="Credentials"/>
</menu>
Поскольку мы используем Toolbar виджет, вам нужно отключить action bar по умолчанию. Для этого измените xml style файл так, как показано ниже.
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Измените код своей activity на следующий.
package com.vogella.android.retrofitgithub;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity implements CredentialsDialog.ICredentialsDialogListener {
GithubAPI githubAPI;
String username;
String password;
Spinner repositoriesSpinner;
Spinner issuesSpinner;
EditText commentEditText;
Button sendButton;
Button loadReposButtons;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar);
setSupportActionBar(toolbar);
sendButton = (Button) findViewById(R.id.send_comment_button);
repositoriesSpinner = (Spinner) findViewById(R.id.repositories_spinner);
repositoriesSpinner.setEnabled(false);
repositoriesSpinner.setAdapter(new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"No repositories available"}));
repositoriesSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (parent.getSelectedItem() instanceof GithubRepo) {
GithubRepo githubRepo = (GithubRepo) parent.getSelectedItem();
compositeDisposable.add(githubAPI.getIssues(githubRepo.owner, githubRepo.name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(getIssuesObserver()));
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
issuesSpinner = (Spinner) findViewById(R.id.issues_spinner);
issuesSpinner.setEnabled(false);
issuesSpinner.setAdapter(new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"Please select repository"}));
commentEditText = (EditText) findViewById(R.id.comment_edittext);
loadReposButtons = (Button) findViewById(R.id.loadRepos_button);
createGithubAPI();
}
@Override
protected void onStop() {
super.onStop();
if (compositeDisposable != null && !compositeDisposable.isDisposed()) {
compositeDisposable.dispose();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_credentials:
showCredentialsDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showCredentialsDialog() {
CredentialsDialog dialog = new CredentialsDialog();
Bundle arguments = new Bundle();
arguments.putString("username", username);
arguments.putString("password", password);
dialog.setArguments(arguments);
dialog.show(getSupportFragmentManager(), "credentialsDialog");
}
private void createGithubAPI() {
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.registerTypeAdapter(GithubRepo.class, new GithubRepoDeserializer())
.create();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.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(username, password));
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
}).build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(GithubAPI.ENDPOINT)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
githubAPI = retrofit.create(GithubAPI.class);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.loadRepos_button:
compositeDisposable.add(githubAPI.getRepos()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(getRepositoriesObserver()));
break;
case R.id.send_comment_button:
String newComment = commentEditText.getText().toString();
if (!newComment.isEmpty()) {
GithubIssue selectedItem = (GithubIssue) issuesSpinner.getSelectedItem();
selectedItem.comment = newComment;
compositeDisposable.add(githubAPI.postComment(selectedItem.comments_url, selectedItem)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(getCommentObserver()));
} else {
Toast.makeText(MainActivity.this, "Please enter a comment", Toast.LENGTH_LONG).show();
}
break;
}
}
private DisposableSingleObserver<List<GithubRepo>> getRepositoriesObserver() {
return new DisposableSingleObserver<List<GithubRepo>>() {
@Override
public void onSuccess(List<GithubRepo> value) {
if (!value.isEmpty()) {
ArrayAdapter<GithubRepo> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, value);
repositoriesSpinner.setAdapter(spinnerAdapter);
repositoriesSpinner.setEnabled(true);
} else {
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"User has no repositories"});
repositoriesSpinner.setAdapter(spinnerAdapter);
repositoriesSpinner.setEnabled(false);
}
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "Can not load repositories", Toast.LENGTH_SHORT).show();
}
};
}
private DisposableSingleObserver<List<GithubIssue>> getIssuesObserver() {
return new DisposableSingleObserver<List<GithubIssue>>() {
@Override
public void onSuccess(List<GithubIssue> value) {
if (!value.isEmpty()) {
ArrayAdapter<GithubIssue> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, value);
issuesSpinner.setEnabled(true);
commentEditText.setEnabled(true);
sendButton.setEnabled(true);
issuesSpinner.setAdapter(spinnerAdapter);
} else {
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, new String[]{"Repository has no issues"});
issuesSpinner.setEnabled(false);
commentEditText.setEnabled(false);
sendButton.setEnabled(false);
issuesSpinner.setAdapter(spinnerAdapter);
}
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "Can not load issues", Toast.LENGTH_SHORT).show();
}
};
}
private DisposableSingleObserver<ResponseBody> getCommentObserver() {
return new DisposableSingleObserver<ResponseBody>() {
@Override
public void onSuccess(ResponseBody value) {
commentEditText.setText("");
Toast.makeText(MainActivity.this, "Comment created", Toast.LENGTH_LONG).show();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "Can not create comment", Toast.LENGTH_SHORT).show();
}
};
}
@Override
public void onDialogPositiveClick(String username, String password) {
this.username = username;
this.password = password;
loadReposButtons.setEnabled(true);
}
}
Здесь мы добавили созданный ранее GithubRepoDeserializer в качестве TypeAdapter в GsonBuilder. Чтобы обрабатывать аутентификацию для каждого вызова, добавили Interceptor в OkHttpClient. Чтобы методы API-интерфейса возвращали типы RxJava2, добавили RxJava2 CallAdapter к своему клиенту.
10. Упражнение: использование Retrofit с OAuth для запроса сведений о пользователе из Twitter на Android
В этом упражнении описывается, как войти в Twitter с помощью Retrofit на Android. Мы напишем приложение, которое может запрашивать и отображать данные пользователя для предоставленного имени пользователя. В этом упражнении мы используем аутентификацию Twitter application-only с OAuth 2 для авторизации. Чтобы это сделать это упражнение, вам необходимо иметь учетную запись Twitter. Кроме того, вам нужно перейти к приложениям Twitter и создать новое приложение, чтобы получить свой consumer key и сonsumer secret. Нам понадобится это позже, чтобы запросить наш токен OAuth.
10.1. Настройка проекта
Создайте приложение для Android с именем Retrofit Twitter. Используйте com.vogella.android.retrofittwitter как имя пакета верхнего уровня.
Чтобы использовать Retrofit, добавьте следующие строки в файл build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
Добавьте разрешение на доступ к Интернету в манифест.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vogella.android.retrofittwitter">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
10.2. Определение API
Создайте следующие два класса данных, которые называются OAuthToken и UserDetails.
package com.vogella.android.retrofittwitter;
import com.google.gson.annotations.SerializedName;
public class OAuthToken {
@SerializedName("access_token")
private String accessToken;
@SerializedName("token_type")
private String tokenType;
public String getAccessToken() {
return accessToken;
}
public String getTokenType() {
return tokenType;
}
public String getAuthorization() {
return getTokenType() + " " + getAccessToken();
}
}
package com.vogella.android.retrofittwitter;
public class UserDetails {
private String name;
private String location;
private String description;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
Класс OAuthToken используется для хранения bearer token, который мы запрашиваем у Twitter, с нашим ключом и тайной. Мы используем аннотацию @ SerializedName для установки имени Retrofit для (де)сериализации полей.
Класс UserDetails просто сохраняет несколько полей из ответа Twitter при запросе данных о пользователе. Мы не показываем все данные пользователя, которые содержались в ответе, только имя, местоположение, URL и описание.
Определите REST API для Retrofit через следующий интерфейс:
package com.vogella.android.retrofittwitter;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface TwitterApi {
String BASE_URL = "https://api.twitter.com/";
@FormUrlEncoded
@POST("oauth2/token")
Call<OAuthToken> postCredentials(@Field("grant_type") String grantType);
@GET("/1.1/users/show.json")
Call<UserDetails> getUserDetails(@Query("screen_name") String name);
}
10.3. Создание Activity
Измените файл activity_main.xml и соответствующий класс MainActivity следующим образом:
<?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="com.vogella.android.retrofittwitter.MainActivity">
<LinearLayout
android:id="@+id/username_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="horizontal">
<TextView
android:id="@+id/username_textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:enabled="false"
android:gravity="center_vertical"
android:text="Username:" />
<EditText
android:id="@+id/username_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:gravity="center"
android:imeOptions="actionDone"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>
<Button
android:id="@+id/request_token_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:onClick="onClick"
android:text="Request token" />
<Button
android:id="@+id/request_user_details_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/request_token_button"
android:enabled="false"
android:onClick="onClick"
android:text="Request user details" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/request_user_details_button"
android:layout_below="@id/username_container"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Name:" />
<TextView
android:id="@+id/name_textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="---" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Location:" />
<TextView
android:id="@+id/location_textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="---" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Url:" />
<TextView
android:id="@+id/url_textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="---" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="Description:" />
<TextView
android:id="@+id/description_textview"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="---" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
package com.vogella.android.retrofittwitter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity {
private String credentials = Credentials.basic("aConsumerKey", "aSecret");
Button requestTokenButton;
Button requestUserDetailsButton;
EditText usernameEditText;
TextView usernameTextView;
TextView nameTextView;
TextView locationTextView;
TextView urlTextView;
TextView descriptionTextView;
TwitterApi twitterApi;
OAuthToken token;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
requestTokenButton = (Button) findViewById(R.id.request_token_button);
requestUserDetailsButton = (Button) findViewById(R.id.request_user_details_button);
usernameEditText = (EditText) findViewById(R.id.username_edittext);
usernameTextView = (TextView) findViewById(R.id.username_textview);
nameTextView = (TextView) findViewById(R.id.name_textview);
locationTextView = (TextView) findViewById(R.id.location_textview);
urlTextView = (TextView) findViewById(R.id.url_textview);
descriptionTextView = (TextView) findViewById(R.id.description_textview);
createTwitterApi();
}
private void createTwitterApi() {
OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder().header("Authorization",
token != null ? token.getAuthorization() : credentials);
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
}).build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(TwitterApi.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
twitterApi = retrofit.create(TwitterApi.class);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.request_token_button:
twitterApi.postCredentials("client_credentials").enqueue(tokenCallback);
break;
case R.id.request_user_details_button:
String editTextInput = usernameEditText.getText().toString();
if (!editTextInput.isEmpty()) {
twitterApi.getUserDetails(editTextInput).enqueue(userDetailsCallback);
} else {
Toast.makeText(this, "Please provide a username", Toast.LENGTH_LONG).show();
}
break;
}
}
Callback<OAuthToken> tokenCallback = new Callback<OAuthToken>() {
@Override
public void onResponse(Call<OAuthToken> call, Response<OAuthToken> response) {
if (response.isSuccessful()) {
requestTokenButton.setEnabled(false);
requestUserDetailsButton.setEnabled(true);
usernameTextView.setEnabled(true);
usernameEditText.setEnabled(true);
token = response.body();
} else {
Toast.makeText(MainActivity.this, "Failure while requesting token", Toast.LENGTH_LONG).show();
Log.d("RequestTokenCallback", "Code: " + response.code() + "Message: " + response.message());
}
}
@Override
public void onFailure(Call<OAuthToken> call, Throwable t) {
t.printStackTrace();
}
};
Callback<UserDetails> userDetailsCallback = new Callback<UserDetails>() {
@Override
public void onResponse(Call<UserDetails> call, Response<UserDetails> response) {
if (response.isSuccessful()) {
UserDetails userDetails = response.body();
nameTextView.setText(userDetails.getName() == null ? "no value" : userDetails.getName());
locationTextView.setText(userDetails.getLocation() == null ? "no value" : userDetails.getLocation());
urlTextView.setText(userDetails.getUrl() == null ? "no value" : userDetails.getUrl());
descriptionTextView.setText(userDetails.getDescription().isEmpty() ? "no value" : userDetails.getDescription());
} else {
Toast.makeText(MainActivity.this, "Failure while requesting user details", Toast.LENGTH_LONG).show();
Log.d("UserDetailsCallback", "Code: " + response.code() + "Message: " + response.message());
}
}
@Override
public void onFailure(Call<UserDetails> call, Throwable t) {
t.printStackTrace();
}
};
}
Замените aConsumerKey и aSecret на consumer key и secret, полученные от Twitter.
Также взгляните на перехватчик, который мы добавляем к нашему Retrofit клиенту. Поскольку мы используем OAuth, наши учетные данные различаются для каждого вызова. Метод postCredentials должен размещать учетные данные (consumer key и secret) в Basic схеме для Twitter. В результате этот вызов возвращает bearer token, который Retrofit десериализует в наш класс OAuthToken, который затем сохраняется в поле токена. Любой другой запрос может (и должен) теперь использовать этот токен в качестве учетных данных для авторизации. Также запрашивается информация о пользователе.
11. Retrofit ресурсы
Consuming APIs with Retrofit tutorial
In dept blog series about Retrofit
Consuming APIs with Retrofit
Комментарии (10)
Apx
04.11.2018 18:49Если я ничего не путаю baseUrl метод билдера клиента всегда вроде требовал '/' в конце урла или это уже убрали в последних версиях?
Apx
04.11.2018 19:00Да, правильно помню таки https://github.com/square/retrofit/blob/master/retrofit/src/main/java/retrofit2/Retrofit.java#L515
Поправьте в статье чтобы у новичков паники не возникло :)
Jukobob
05.11.2018 09:07При использовании этого адаптера интерфейсы Retrofit могут возвращать типы RxJava 2.x, например, Observable, Flowable или Single и т. д.
Приведите пожалуйста примеры, когда идиоматически правильно использовать Flowable & Observable в качестве возвращаемого параметра у интерфейса.strorinWind Автор
05.11.2018 10:10Поскольку это перевод, а не мое авторство, и я пока не очень хорошо разбираюсь в Rx, думаю, я не в праве давать такие комментарии. Будет здорово, если кто-то другой ответит
Различия между типами можно посмотреть например здесь
alunicus
05.11.2018 12:33Для получения данных из API лучше использовать Single или Maybe(в случае если с сервера может прийти пустое тело в ответе), т.к обычно запрос один и подписка не нужна. Разница же между Flowable и Observable в RxJava 2.x в том, что у первого есть BackPressure, а у второго нет. Подробнее про Backpressure c примерами можно почитать здесь.
Jukobob
06.11.2018 06:11Поправлю Вас немножко. Если ожидаете пустое тело ответа то, идеологически правильно, было бы использовать Completable. Maybe нужно использовать для операция другого типа.
К чему я это все?
Когда я вижу в примерах Retrofit'a Observable меня просто в дрожь бросает. Вроде бы ничего плохого и «и так сойдет» но дьявол кроется в мелочах.alunicus
06.11.2018 11:01Да, Вы правы насчет Maybe, спасибо за уточнение.
Мне кажется, большинство таких примеров пришли к нам прямиком из RxJava 1.x, причем еще до появления в ней Single и т.д. Переписывать все примеры у авторов либо нет времени, либо «и так сойдет» :) Ну а часть статей вообще бездумно скопированы, цифра 1 поменяна на 2 и добавлена строчка про Flowable. Так что найти хорошее объяснение на реальном примере — это надо еще очень постараться.
Handen
Спасибо за статью.
Объясните новичку, зачем использовать Retrofit и прочие полезные библиотеки, если можно всё накодить без них?
strorinWind Автор
Для ускорения скорости разработки :)
Кто-то уже написал то, что скорее всего понадобится в большинстве проектов.
В случае с Retrofit, после того как один раз создан и настроен Retrofit клиент добавление еще одного запроса к Api занимает пару строк и пару минут. Кроме того не приходится заботиться об асинхронности самостоятельно (в андроид нельзя выполнять запросы к сети из главного (UI) потока), а без сторонних библиотек это требует много усилий и нервов :D