Изучаем 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