Изучаем Retrofit 2
В мире Android разработки существует множество интересных библиотек, и сегодня мы рассмотрим детище компании Square — Retrofit. Что же это за зверь такой?
Retrofit (согласно официальному сайту) — типобезопасный HTTP-клиент для Android и Java. Он является незаменимым инструментом для работы с API в клиент-серверных приложениях. Каких-то лет 5 назад Android-разработчикам для работы с сетью приходилось воротить горы кода с обратными вызовами, AsyncTask'ами и прочими «низкоуровневыми» вещами. И компания Square выпустила такую замечательную библиотеку — Retrofit.
В сети Интернет мне не удалось найти внятных туториалов по второй версии библиотеки (на ноябрь 2016 года), поэтому сегодня мы будем разбираться с ней на примере приложения, получающего посты с bash.im.
Лучше один раз увидеть, чем сто раз услышать
Мы будем создавать приложение, получающее данные от API сайта Umorili, так как только они предоставляют данные с баша в удобном для парсинга виде. Вот так будет выглядеть конечный вариант:
Дизайном, конечно, не блещет.
Ну что, вы готовы, дети?
Зависимости
Библиотеку Retrofit можно подключить тремя способами: с помощью Gradle, Maven и Jar. Опишем каждый способ.
Gradle
В большинстве случаев для сборки приложений под Android используется именно этот инструмент, поэтому, если вы не уверены, берите этот вариант :) (здесь и далее будут использоваться зависимости Gradle).
Для подключения в файл build.gradle модуля приложения в раздел dependencies вставляем строчку:
compile 'com.squareup.retrofit2:retrofit:2.1.0'
Maven
Если кто-то использует эту систему зависимостей и сборки, то фрагмент зависимости будет выглядеть так:
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.1.0</version>
</dependency>Jar
Не приветствую использование этого варианта, но некоторые любят его. Скачиваем с официального сайта jar-файл (ссылка) и кидаем его в папку libs
Помимо самой библиотеки нам понадобится парсер JSON и RecyclerView-v7, поэтому подключим их:
compile 'com.squareup.retrofit2:converter-gson:2.1.0' //Конвертер JSON //можно, если предпочитаете, использовать Jackson compile 'com.android.support:recyclerview-v7:25.0.0' //RecyclerView
С зависимостями разобрались, теперь перейдем к самой сладкой части — разработке. Перво-наперво нам нужно описать запросы к API.
Описание запросов к API
Retrofit позволяет сделать полноценный REST-клиент, который может выполнять POST, GET, PUT, DELETE запросы. Для обозначения типа и других аспектов запроса используются аннотации. Например, для того чтобы обозначить, что требуется GET запрос, нам нужно написать перед методом аннотацию @GET, для POST запроса – @POST, и так далее. В скобках к типу запроса ставится целевой адрес. Для примера возьмем API GitHub'а. Полный URL для получения списка репозиториев определенного пользователя можно представить в виде https://api.github.com/users/octocat/repos, где:
api.github.com— базовая часть адреса (всегда оканчивается слешем);users/{user}/repos— метод (адрес документа, целевой адрес), где определенного пользователя (octocat) мы заменили на алиас (про использование алиасов чуть позже).
Еще существуют параметры запроса, например в запросе к Umorili мы будем использовать следующий адрес — http://www.umori.li/api/get?name=bash&num=50, где name=bash&num=50 — параметры.
Но одними аннотациями описание не заканчивается – нам же надо где-то их описать. А описываем мы их в интерфейсе (interface). Для нашего приложения интерфейс будет следующим:
import java.util.List
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
import ru.mustakimov.retrofittutorial.PostModel
interface UmoriliApi {
@GET("/api/get")
fun getData(
@Query("name") resourceName: String,
@Query("num") count: Int
): Call<List<PostModel>>
}Разберем этот интерфейс. У нас есть метод getData, возвращающий объект типа Call<List<PostModel>>. Методы должны всегда возвращать объект типа Call<T> и иметь аннотацию типа запроса (GET, POST, PUT, DELETE).
Аннотация @Query("name") resourceName: String показывает Retrofit'у, что в качестве параметра запроса нужно поставить пару name=<Значение строки resourceName>.
Если у нас в целевом адресе стоит алиас, то, для того чтобы заместо алиаса поставить значение, нам нужно в параметрах функции написать @Path("<Название алиаса>") variable: SomeType, где SomeType — любой тип (например, String, Int, Float).
PostModel— класс, сгенерированный сайтом jsonschema2pojo на основе ответа сервера, преобразованный в Kotlin и немного упрощенный:
data class PostModel(
val site: String,
val name: String,
val desc: String,
val link: String,
val elementPureHtml: String,
)Подготовка к запросу
Перед отправкой запроса и получением результата нам нужно произвести инициализацию Retrofit'а и объекта интерфейса. Чтобы приложение не имело сотню объектов, выполняющих одну и ту же функцию, мы произведем всю инициализацию в классе, унаследованном от Application. Код этого класса тогда будет следующим:
import android.app.Application
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import ru.mustakimov.retrofittutorial.api.UmoriliApi
class App : Application() {
override fun onCreate() {
super.onCreate()
val retrofit = Retrofit.Builder()
//Базовая часть адреса
.baseUrl("http://www.umori.li/")
//Конвертер, необходимый для преобразования JSON'а в объекты
.addConverterFactory(GsonConverterFactory.create())
.build()
//Создаем объект, при помощи которого будем выполнять запросы
umoriliApi = retrofit.create(UmoriliApi.class)
}
companion object {
private lateinit var umoriliApi: UmoriliApi
fun getApi() = umoriliApi
}
}P.S. не забываем в манифесте прописать, что используем свой класс Application
Теперь из любого класса мы имеем доступ к API.
Получение данных
Мы можем выполнять запросы (и, следовательно, получать данные) двумя способами — синхронными или асинхронными запросами. Для синхронного (блокирующего) получения мы используем метод execute() у объекта типа Call. Для нашего примера код был бы следующим:
val response = App.getApi().getData("bash", 50).execute()В результате выполнения мы получаем объект типа Response (ответ), откуда мы можем уже получить распарсенный ответ методом body().
Для асинхронного получения мы заменяем execute() на enqueue(), где в параметрах передаем функции обратного вызова (колбэки). В нашем примере будет выглядеть примерно так:
App.getApi().getData("bash", 50).enqueue(object : Callback<List<PostModel>>() {
override fun onResponse(call: Call<List<PostModel>>, response: Response<List<PostModel>>) {
//Данные успешно пришли, но надо проверить response.body() на null
}
override fun onFailure(call: Call<List<PostModel>>, t: Throwable) {
//Произошла ошибка
}
})Отображение данных
Данные мы уже получили, а как их теперь отобразить? Кидаем в разметку активности RecyclerView и как-нибудь его обзываем. После этого создаем разметку для элемента.
Вот что получилось у меня:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="ru.mustakimov.retrofittutorial.MainActivity">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:id="@+id/posts_recycle_view"
android:layout_alignParentStart="true" />
</RelativeLayout>post_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/postitem_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Очень интересный пост с баша, который никто никогда не видел, так как его не существует"
android:textColor="?android:attr/textColorPrimary"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:id="@+id/postitem_site"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bash.im"
android:layout_below="@+id/postitem_post"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end"
android:textAlignment="textEnd" />
</RelativeLayout>После создаем адаптер для RecyclerView:
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
class PostsAdapter
private val posts: List<PostModel>
) : RecyclerView.Adapter<PostsAdapter.ViewHolder>() {
override fun onCreateViewHolder(ViewGroup parent, int viewType): ViewHolder {
val v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.post_item, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = posts.get(position)
holder.post.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(
post.elementPureHtml,
Html.FROM_HTML_MODE_LEGACY
)
} else {
Html.fromHtml(post.elementPureHtml)
}
holder.site.text = post.site;
}
override fun getItemCount() = posts?.size() ?: 0
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val post = itemView.findViewById<TextView>(R.id.postitem_post)
val site = itemView.findViewById<TextView>(R.id.postitem_site)
}
}И прописываем в MainActivity инициализацию адаптера RecyclerView, а также получение данных:
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.widget.Toast
import java.io.IOException
import java.util.ArrayList
import java.util.List
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
lass MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private var posts = mutableListOf<PostModel>()
override fun onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViwById(R.id.posts_recycle_view)
val layoutManager = LinearLayoutManager(this)
recyclerView.setLayoutManager(layoutManager)
val adapter = new PostsAdapter(posts)
recyclerView.adapter = adapter
try {
val response = App.getApi().getData("bash", 50).execute()
} catch (e: IOException) {
e.printStackTrace()
}
App.getApi().getData("bash", 50).enqueue(object : Callback<List<PostModel>>() {
override fun onResponse(call: Call<List<PostModel>>, response: Response<List<PostModel>>) {
posts.addAll(response.body())
recyclerView.adapter.notifyDataSetChanged()
}
override fun onFailure(call: Call<List<PostModel>>, t: Throwable) {
Toast.makeText(MainActivity.this,
"An error occurred during networking", Toast.LENGTH_SHORT).show()
}
})
}
}На GitHub'е вы можете найти полный код данного приложения (на Java).
Источник: Изучаем Retrofit 2