October 10, 2019

Изучаем Retrofit 2

В мире Android разработки существует множество интересных библиотек, и сегодня мы рассмотрим детище компании SquareRetrofit. Что же это за зверь такой?

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