image

Не давно на хабре была статья в которой предлагалось сделать 8 учебных проектов. Мне там приглянулся трекер криптовалют, дабы было хоть как-то интереснее чем просто Get запрос, было решено сделать его на Kotlin. Итак, в этом туториале вы узнаете следующее:

  • Как делать Get запросы с Retrofit
  • Retrofit и Rx
  • RecyclerView с Котлином
  • Извлечение данных с api

Введение


Упустим то как включить поддержку Котлина и прочие очевидные и понятные вещи, вроде создания проекта. Мы будем использовать вот это api

Настройка Manifest


Для того чтобы делать запросы нужно иметь разрешение на использование сети:

<uses-permission android:name="android.permission.INTERNET"/>

Добавление библиотек в Gradle


Нам понадобится Retrofit и RxAndroid.

 //Retrofit
    compile "com.squareup.retrofit2:retrofit:2.3.0"
    compile "com.squareup.retrofit2:adapter-rxjava2:2.3.0"
    compile "com.squareup.retrofit2:converter-gson:2.3.0"

    //RxAndroid
    compile "io.reactivex.rxjava2:rxandroid:2.0.1"  

Создаем макеты


activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.kram.vlad.cryptocurrency.activitys.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>


RecyclerView должен знать как выглядят его элементы для этого нам нужно создать item.xml.

item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@drawable/ic_launcher_background"/>

            <TextView
                android:id="@+id/symbol"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_marginStart="11dp"
                android:layout_marginTop="11dp"
                android:layout_toEndOf="@+id/imageView"
                android:text="TextView"
                android:textColor="@android:color/black"
                android:textSize="18sp"
                android:textStyle="bold"/>

            <TextView
                android:id="@+id/textView2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/symbol"
                android:layout_marginStart="11dp"
                android:layout_toEndOf="@+id/symbol"
                android:text="|"
                android:textColor="@android:color/black"
                android:textSize="18sp"/>

            <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/textView2"
                android:layout_marginStart="12dp"
                android:layout_toEndOf="@+id/textView2"
                android:text="TextView"
                android:textColor="@android:color/black"
                android:textSize="14sp"/>

            <TextView
                android:id="@+id/money"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/name"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="13dp"
                android:text="TextView"
                android:textColor="@android:color/black"
                android:textSize="14sp"
                android:textStyle="bold"/>

            <TextView
                android:id="@+id/textView6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/imageView"
                android:layout_marginBottom="10dp"
                android:layout_marginLeft="20dp"
                android:layout_toEndOf="@+id/imageView"
                android:text="24h:"/>

            <TextView
                android:id="@+id/textView7"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textView6"
                android:layout_alignBottom="@+id/textView6"
                android:layout_alignEnd="@+id/name"
                android:text="7d:"/>

            <TextView
                android:id="@+id/hours"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textView6"
                android:layout_alignBottom="@+id/textView6"
                android:layout_toEndOf="@+id/textView6"
                android:text="-2.94%"
                android:textStyle="bold"/>

            <TextView
                android:id="@+id/days"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/textView7"
                android:layout_toEndOf="@+id/textView7"
                android:text="+10.19%"
                android:textStyle="bold"/>
        </RelativeLayout>
    </android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>


Делаем модель для парсинга ответа


Для таких целей неплохо подойдет какой-нибудь pojo генератор.

ResponseItrem
data class ResponseItem(@SerializedName("id")
                        @Expose var id: String?,
                        @SerializedName("name")
                        @Expose var name: String?,
                        @SerializedName("symbol")
                        @Expose var symbol: String?,
                        @SerializedName("rank")
                        @Expose var rank: String?,
                        @SerializedName("price_usd")
                        @Expose var priceUsd: String?,
                        @SerializedName("price_btc")
                        @Expose var priceBtc: String?,
                        @SerializedName("24h_volume_usd")
                        @Expose var _24hVolumeUsd: String?,
                        @SerializedName("market_cap_usd")
                        @Expose var marketCapUsd: String?,
                        @SerializedName("available_supply")
                        @Expose var availableSupply: String?,
                        @SerializedName("total_supply")
                        @Expose var totalSupply: String?,
                        @SerializedName("max_supply")
                        @Expose var maxSupply: String?,
                        @SerializedName("percent_change_1h")
                        @Expose var percentChange1h: String?,
                        @SerializedName("percent_change_24h")
                        @Expose var percentChange24h: String?,
                        @SerializedName("percent_change_7d")
                        @Expose var percentChange7d: String?,
                        @SerializedName("last_updated")
                        @Expose var lastUpdated: String?) {


Создаем простой Get запрос


Мы будем использовать Rx поэтому наша Get функция должна возвращать Observable. Также прямо здесь мы создаем Factory, с него будем получать объект Retrofit'а.

GetInterface
interface RetrofitGetInterface {
    @GET("v1/ticker/")
    fun getCryptocurrency(): Observable<List<ResponseItem>>


    companion object Factory {
        fun create(): RetrofitGetInterface {
            val retrofit = Retrofit.Builder()
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create()) // говорим чем парсим 
                    .baseUrl("https://api.coinmarketcap.com/") // базовая часть ссылки
                    .build()

            return retrofit.create(RetrofitGetInterface::class.java)
        }
    }
}


Делаем запрос


Для запросов мы будем использовать Rx. Если вы не знакомы с реактивным программированием — вы не знаете что теряете.

Код для запроса
val apiService = RetrofitGetInterface.create()
        apiService.getCryptocurrency() 
                .observeOn(AndroidSchedulers.mainThread())// Говорим в какой поток вернуть
                .subscribeOn(Schedulers.io()) // Выбераем io - для работы с данными и интернетом
                .subscribe({
                    result -> arrayListInit(result) // Здесь у нас калбек
                }, { error ->
                    error.printStackTrace()
                })


Делаем адаптер для списка



class RecyclerViewAdapter(private val result: List<ResponseItem>, 
val resources: Resources):RecyclerView.Adapter<RecyclerViewAdapter.CardViewHolder>()

Данные которые мы получили с нашего запроса нужно засунуть в какой-нибудь массив(List) ResponseItem. Его нужно передать в адаптер, чтобы он наполнял наш список.

В GetItemCount мы должны возвращать размер массива для того чтобы адаптер знал сколько элементов будет в нашем списке.

override fun getItemCount() = result.size //Возвращаем размер масива данных

В OnCreateViewHolder мы должны инфлейтнуть макет нашего итема.

   
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): CardViewHolder {
        return CardViewHolder(LayoutInflater.from(parent?.context)
                .inflate(R.layout.item, parent, false)) //Говорим RecyclerView как должен выглядеть item
}

Все время в коде светится какой-то CardViewHolder. Он должен наполнять вьюхи каждого итема данными из массива.

 class CardViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {

        fun bind(result: List<ResponseItem>, position: Int, resources: Resources) {
            val responseItem: ResponseItem = result.get(position)

            itemView.symbol.text = responseItem.symbol
            itemView.name.text = responseItem.name
            ...
        }
}

Функция bind берет все необходимые данные и наполняет ими вьюхи. К этой функции мы обращаемся в onBindViewHolder. И благодаря синтаксису языка делаем это довольно красиво.

override fun onBindViewHolder(holder: CardViewHolder, position: Int)
 = holder.bind(result, position, resources)

Последний шаг: прикрепляем наш адаптер


Для этого в калбеке нужно просто прописать:

recyclerView.adapter = RecyclerViewAdapter(result, resources)
recyclerView.layoutManager = LinearLayoutManager(this)

Исходники здесь. Туториал вышел довольно коротким и простым, поэтому объяснений было немного, но если что-то непонятно могу ответить, и все же у нас есть готовый трекер криптовалют.

Комментарии (16)


  1. gshock
    14.12.2017 16:45

    Немного странно видеть вложенный в ConstraintLayout CardView, который для размещения вложенных компонентов использует RelativeLayout


  1. maxzh83
    14.12.2017 17:11

    Картинка с iPhone X специально добавлена, чтобы заходили сюда и задавали этот вопрос?


    1. vlad2711 Автор
      14.12.2017 17:17

      Нет. Это картинка с той статьи, ссылку на которую я указал вначале, она там была в качестве примера.


  1. molchanoviv
    14.12.2017 17:54

    Сразу видно Джависта перешедшего на Котлин, но пока-еще не научившегося писать идеоматичный код.

    P.S. Зачем тянуть в проект Rx только для отправки запроса? Чем не подошли корутины? Они проще, код с ними чище, и работают из коробки. Плюс не нужно знать 100500 операторов сторонней библиотеки. Я могу понять зачем Rx в Java, но вот в Kotlin — нет.


    1. vlad2711 Автор
      14.12.2017 17:56

      Про корутины спасибо. Не знал, ограничился синтаксисом, а вот глубже копнуть было лень:(


    1. Jukobob
      15.12.2017 11:12

      Coroutines — это способ писать асинхронный код в функциональном стиле. Также помогает избежать `Callback Hell`
      Rx — это больше к логике. Когда вы стоите потоки данных и управляете ими с помощью операторов.

      Если на собеседовании у вас спросят «Зачем вы используете Rx в Android Application» и вы ответите «Для асинхронности», то это будет ну очень плохой ответ (от слова совсем). Rx и Coroutines это вообще из разных отраслей программирования.


  1. abbath0767
    14.12.2017 17:55

    Я вот понять не могу — что несет эта статья в массы? Может рассказывает, что то новое, что то интересное или может хотя бы просто оригинальное решение старой задачи новым способом?


    1. artem_dobrovinskiy
      14.12.2017 21:27

      В одной из прошлых статей по теме автор спросил — переводить ли эту часть. Люди попросили перевести — автор уважил.


    1. Space_Cowboy
      15.12.2017 13:09

      Как что? Рекламу kotlin конечно же!


      1. vlad2711 Автор
        15.12.2017 13:10

        Это не реклама. Просто для людей которые хотят юзать котлин, но не знают с чего начать создан этот простой туториал.


        1. abbath0767
          15.12.2017 14:01

          Да достаточно уже информации по котлину, хорошей правда к сожалению мало, но для того что бы «начать» более чем достаточно. Просто оказавшись хотя бы в радиусе 100 метров от любого андроид разработчика достаточно сказать шепотом «Котлин или джава...» и тебя научат, покажут, расскажут и похоливарят на пустом месте.

          P.S. Вот написали бы тоже самое на дарте с флаттером, более ответственно и с подробностями техническими — я уверен статья была бы полезнее. Но это не точно


  1. qwert_ukg
    15.12.2017 05:59

    override fun getItemCount(): Int {
            return result.size //Возвращаем размер массива данных
    }

    Так вроде получше


    override fun getItemCount() = result.size //Возвращаем размер массива данных


  1. qwert_ukg
    15.12.2017 06:10

    На сколько я помню, JB готовили типобезопасный билдер для описания макетов, нечто вроде kotlinx.html, чтобы избавить всех от xml


    1. vlad2711 Автор
      15.12.2017 10:48

      В принципе это гуд, ведь сейчас в любой нормальной приложухе один скрин разрастается на сотни и тысячи строк xml что реально просто не может не бесить, ведь в этой всей каше найти что-то и поменять занимает немало времени.


      1. qwert_ukg
        15.12.2017 11:03

        Как по мне, так вся прелесть билдера именно в типобезопасности, нельзя описать описать, например TextView вне RelativeLayout


  1. Jukobob
    15.12.2017 11:03

    Хотел промолчать, извините, но не могу. Это боль для моих глаз и код ревью этой статьи.
    Последней каплей стало это

    Мы будем использовать Rx поэтому наша Get функция должна возвращать Observable.

    GET(«v1/ticker/»)
    fun getCryptocurrency(): Observable<List>

    Это прям бельмо на глазу. 95% людей на моей практике не понимают идеологию Rx.
    Observable — наблюдаемые потоки данных, в которых вы допускаете больше одного вызова onNext. Обьясните мне, как запрос к сети может спровоцировать подобное действие? Ну естественно про существование `Single` и `Complitable` люди вообще не знают.

    Далее по списку
    class CardViewHolder(itemView: View?) :

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): CardViewHolder {
    return CardViewHolder(LayoutInflater.from(parent?.context)


    Почему вы допускате `nullable` в parent и view? Что произойдет если parent == null?

    Ладно, последний вопрос, что это вообще за DTO?

    data class ResponseItem(@SerializedName(«id»)
    @Expose var id: String?,
    @SerializedName(«name»)
    @Expose var name: String?,


    Вы уверены на счет повсеместного nullabe и необходимости использования `@Expose` анотации?

    К сожалению, пока что это больше похоже на сборник плохих примеров как использовать Kotlin RxJava и Retrofit