Наверное, из тех, кто интересуется разработкой под Android, только ленивый не слышал про Kotlin. На хабре про него уже писали: вот статья, вот видео с MBLTdev. Язык активно развивается, но новых статей все нет. Я решил, что пора это исправить.

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

Anko


Несомненно, очень важная библиотека для любого пользователя Kotlin, которая пока еще только развивается (текущая версия 0.6). Рассмотрим ее возможности.

Anko DSL

Как известно, создавать интерфейс можно не только с помощью xml, но и прямо в Java-коде. Однако, это адская боль не очень легко. Библиотека Anko предоставляет другой способ создания интерфейса пользователя, который имеет немало общего с билдерами в Groovy (впрочем, в Kotlin они тоже есть).
В качестве простого примера перепишем стандартный HelloWorld с xml на Anko DSL:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    relativeLayout {
        padding = dip(16)
        textView("Hello world!") {
        }
    }
}

Не могу не сказать, что я сам был немного удивлен, когда, переписывая xml в Anko DSL, написал эти 5 строчек и неожиданно понял, что это все.
Эквивалентная xml-разметка
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin">

    <TextView
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>


Конечно, такой подход можно применять и для создания более сложного UI:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    relativeLayout {
        padding = dip(16)

        val w = dip(200)
        val loginEditId = 155;
        val loginEdit = editText {
            id = loginEditId
            hint = "Login"
        }.layoutParams { centerInParent(); width = w }

        button("Sign up") {
            textSize = 18f
            onClick { doWork(loginEdit.getText().toString()) }
        }.layoutParams {
            below(loginEditId); sameLeft(loginEditId);
            width = w; topMargin = dip(8)
        }
    }
}

Результат:


У Anko DSL есть еще одна интересная возможность — сокращенная реализация интерфейсов. Например, стандартный интерфейс TextWatcher содержит 3 метода, и нам нужно реализовать их все, даже если мы хотим только один:
val edit = EditText(this)
edit.addTextChangedListener(object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
        toast(s)
    }

    override fun afterTextChanged(s: Editable) {
    }
})

С помощью Anko DSL мы можем реализовывать только то, что нам нужно (write as less code as possible):
val edit = editText {
    textChangedListener {
        onTextChanged { text, start, before, count -> toast(text.toString()) }
    }
}.layoutParams { centerInParent() }


В качестве небольшого заключения могу сказать, что Anko DSL является скорее альтернативой xml, но не однозначной заменой. Заменой он может являться для создания UI из Java-кода. Anko DSL не дает ощутимого преимущества в скорости, поэтому ради производительности использовать его не стоит.

На мой взгляд, использование Anko DSL хорошо тем, что и свойства, и обработчики событий для элементов вы пишите в одном месте (я подразумеваю, что обычно стиль / позиционирование элементов обычно выполняется с помощью xml, а обработчики событий присваиваются в Java коде) — это может давать плюс к логике. Кроме того, можно попробовать использовать Anko DSL в модном паттерне MVP.

Возможно также, что для кого-то такой builders-style является более приятным, чем xml.
И еще один важный момент — Anko только развивается. Возможно, в будущем мы увидим новые крутые фичи от этой библиотеки. Поэтому стоит обратить на нее внимание.

Anko features

Кроме DSL библиотека Anko позволяет нам писать намного меньше стандартного кода.
Рассмотрим некоторые фрагменты кода и то, как Anko позволяет их менять. Я просто буду приводить примеры — комментарии, на мой взгляд, излишни.
  • Сообщения Toast:
    Toast.makeText(this, "Hello", Toast.LENGTH_SHORT).show()
    //=>
    toast("Hello")
    

  • Открытие браузера для просмотра URL:
    val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com"));
    startActivity(browserIntent);
    //=>
    browse("https://www.google.com")
    

  • Запуск другой Activity:
    val intent = Intent(this, javaClass<MainActivity>())
    intent.putExtra("from", "Peter")
    intent.putExtra("to", "Vasya")
    intent.putExtra("message", "hello")
    startActivity(intent)
    //=>
    startActivity<MainActivity>("from" to "Peter", "to" to "Vasya", "message" to "hello")
    

    Обратите внимание на способ передачи параметров. Аналогично создаются объекты типа Map.
  • Показ диалогов:
    val dialog = AlertDialog.Builder(this)
                .setTitle("Exit")
                .setMessage("Do you really want exit?")
                .setPositiveButton("Yes", { dialog, which ->  finish() })
                .setNegativeButton("No", { dialog, which -> dialog.dismiss() })
                .create()
                .show()
    
     //=>
    
    alert("Exit", "Do you really want exit?") {
        positiveButton("Yes") { finish() }
        negativeButton("No") { dismiss() }
    }.show()
    

  • Работа с AsyncTask:
    class MyTask : AsyncTask<Void, Void, Void>() {
        override fun doInBackground(vararg params: Void?): Void? {
            //do some work
            return null
        }
        override fun onPostExecute(result: Void?) {
            toast("Finished")
        }
    }
    val task = MyTask()
    task.execute()
    
    //=>
    
    async {
        //do some work
        uiThread {
            toast("Finished")
        }
    }
    


Таким образом, библиотека Anko предоставляет набор полезных вкусностей, которые несомненно пойдут на пользу вашему проекту на Kotlin. Так что вперед:
compile 'org.jetbrains.anko:anko:0.6.1-19s'

For Android with love


Нельзя не упомянуть и о том, что команда Kotlin-а очень хорошо старается во имя Android и добавляет в язык очень важные для любого Android-разработчика возможности.

Annotation Processing

Буквально несколько дней назад у Android-разработчиков наконец появилась возможность использовать различные DI-фреймворки в сочетании с Kotlin. По этому поводу в блоге разработчиков Kotlin-а появилась статья, в которой объясняются некоторые детали. Кроме того, можно посмотреть пример с использованием Dagger. Пока что данная возможность еще сыровата и будет дорабатываться, но общее направление радует.
Я не считаю, что могу добавить что-то полезное к ссылкам выше, поэтому этот пункт пойдет просто как новость.

Secondary constructors

Изначально в Kotlin планировалось использовать для каждого класса только один primary конструктор. Это аргументировалось тем, что этого всегда достаточно. Вообще, это почти всегда правда. Однако с таким ограничением Android-разработчики не могли создавать собственные View классы.
Теперь это возможно:
public class MyView : LinearLayout {

    public constructor(context: Context) : super(context) {
    }

    public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    }

    //...
}

Естественно, применение secondary конструкторов не ограничивается только View классами.

Delegated properties

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

Делегирование в данном контексте означает передачу обработки поля / переменной определенному классу. Этот класс содержит get и set методы для данного поля. Такая передача позволяет реализовать некоторые концепции, например:
  • Ленивая инициализация;
  • Observable properties — мониторинг и реакция на изменение значения поля.

Kotlin определяет набор стандартных важных делегатов, которые находятся в файле Delegation.kt в объекте Delegates. Например, можно очень легко использовать lazy initialization:
val someBigValue by Delegates.lazy {
    var result = BigInteger.ONE
    //some hard computations
    result
}

При обращении к полю someBigValue в первый раз, его значение будет посчитано и сохранено, во второй и следующей раз будет возвращаться сохраненное значение.
Примечание: не используйте эту конструкцию для создания различных Singleton-ов, для этого в Kotlin есть object declarations.
Другие примеры вы можете найти в документации.

Чем же так замечательно делегирование применительно к Android? Вспоминаем одну из ключевых особенностей Kotlin — null-safety. Обычно мы объявляем элементы UI как поля Activity / Fragment, например, так:
private var mHelloWorldTextView : TextView = //???

Однако в Kotlin мы не можем присвоить полю типа TextView значение null. Поэтому пишем что-то такое:
private var mHelloWorldTextView : TextView? = null

Но это ведет к страшным конструкциям с повсеместным использованием операторов "?." и "!!.", что не добавляет красоты коду:
override fun onCreate(savedInstanceState: Bundle?) {
    //...
    mHelloWorldTextView = findViewById(R.id.helloWorldTextView) as TextView?
    mHelloWorldTextView?.setOnClickListener { /*...*/ }
    val text = mHelloWorldTextView!!.getText()
}

И здесь на помощь спешит очень полезный метод notNull объекта Delegates:
private var mHelloWorldTextView : TextView by Delegates.notNull()

override fun onCreate(savedInstanceState: Bundle?) {
    //...
    mHelloWorldTextView = findViewById(R.id.helloWorldTextView) as TextView
    mHelloWorldTextView.setOnClickListener { /*...*/ }
    val text = mHelloWorldTextView.getText()
}

При попытке обратиться к полю mHelloWorldText перед его инициализацией мы получаем IllegalStateException. Весьма похоже на Optional из Java 8.

UPD 1.
В Activity можно использовать альтернативный вариант:
private val mHelloWorldTextView : TextView by Delegates.lazy { findViewById(R.id.helloWorldTextView) as TextView }

Спасибо пользователю Sp0tted_0wl

UPD 2.
А вообще, для View используйте Kotlin Android Extensions
Спасибо abreslav и kivsiak

Заключение


На этом я закончу рассмотрение основных интересных возможностей Kotlin для Android-разработки. Спасибо, что дочитали до конца.
Изучайте Kotlin, пишите на Kotlin, любите Kotlin!

P.S. Присоединяйтесь к крупнейшему в мире сообществу Android разработчиков в Slack.

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


  1. xGromMx
    25.05.2015 16:10

    Слежу за этим проектом с раннего этапа, еще когда он именовался koan) Имел каверзную дисскусию с автором, было интересно.


    1. xGromMx
      25.05.2015 18:26
      -3

      И опять какая-то незримая фея сыплет пыльцу, на мою карму :)


      1. silentnuke
        25.05.2015 21:20
        +6

        держите нас в курсе ;-)


  1. kivsiak
    25.05.2015 16:50

    Насчет последнего, или пример не удачный но мне не кажется что IllegalStateException будет чем-то принципиально отличаться от NPE. Может этот делагат можно использовать как монаду?


    1. Arturka Автор
      25.05.2015 17:04

      Вы не совсем правильно поняли. Конструкция

      by Delegates.notNull()
      нужна чтобы поле mHelloWorldTextView имело тип TextView, а не TextView?, чтобы не было всяких операторов типа "?." и "!!.".

      Для справки (на всякий случай) — объект типа TextView не может принимать значение null. Для null есть типы с "?" в конце — TextView?.. Это во всем котлине. Если объект имеет тип "?", то к его полям нельзя обращаться непосредственно через "." — код просто не скомпилируется. Для этого есть операторы "?." и "!!." (сложно представить, как можно было додуматься до такого оператора:))


      1. kivsiak
        25.05.2015 17:15
        +1

        Я к тому что по моему опыту textView в Activity, в 90% случаев имеет null значение не потому что ему намерено null присвоили, а потому что забыли инициализировать в onCreate(). И тут Delegates.notNull() ни разу не помощник получается. Потому и говорю про не самый удачный пример.


        1. Arturka Автор
          25.05.2015 17:43

          Опять-таки дело не в этом. Я не зря привел такой пример:

          private var mHelloWorldTextView : TextView = //???
          

          Переменную mHelloWorldTextView нужно чем-то инициализировать. А в данном случае (без Delegates) можно инициализировать только null. Тогда тип mHelloWorldTextView уже будет не TextView, а TextView?, что не хочется.
          Пример предназначен только для этого.

          Впрочем, Kotlin Android Extensions решает все проблемы.


  1. Sp0tted_0wl
    25.05.2015 17:24
    +2

    Гораздо лучше сделать вот так:

    private val mHelloWorldTextView : TextView by Delegates.lazy { findViewById(R.id.helloWorldTextView) as TextView }
    

    Во фрагментах так просто не получится, но в активити самое то.


    1. kivsiak
      25.05.2015 17:30
      +2

      Вообще еще больше мне импонирует kotlinlang.org/docs/tutorials/android-plugin.html которые позволяет вообще избавиться от findViewById


      1. xGromMx
        25.05.2015 17:33
        +1

        Вы также можете использовать это github.com/JakeWharton/kotterknife


      1. Arturka Автор
        25.05.2015 17:34

        Да, это очень сильная вещь, я забыл упомянуть о ней.


  1. kivsiak
    25.05.2015 17:34

    А что как kotlin для андроид сосуществует с DI типа roboguice или dagger? Слышал что теряется совместимость. Может есть какие-то свои альтернативы?


    1. xGromMx
      25.05.2015 18:32

      Может это поможет github.com/damianpetla/kotlin-dagger-example


  1. VioletGiraffe
    26.05.2015 10:59
    +1

    >из тех, кто интересуется разработкой под Android, только ленивый не слышал про Kotlin
    О, наконец-то я достоверно выяснил, что я ленив :)


    1. n0ne
      26.05.2015 11:53
      +1

      День открытий (-:
      Я тоже не слышал (-:


  1. stepango
    27.05.2015 05:31
    +2

    У меня несколько вопросов по Anko DSL.
    Как применять стили?
    Как использовать custom views?
    Есть ли удобный способ брать размеры элеметов из dimen.xml?
    Что делать если понадобится 2 разметки для portrait и landscape?


    1. Arturka Автор
      27.05.2015 09:51

      Хорошие вопросы)
      Во-первых, замечу еще такую вещь, что при открытии файла xml разметки во вкладке Code есть возможность конвертации в Koan DSL. Однако работает пока далеко не очень)

      Теперь по вопросам:
      1) Насколько я знаю, стили, описанные в xml, использовать нельзя. Это в принципе логично, все пишем в DSL (однако с dimen такое не катит, под разные размеры экрана придется в xml писать).
      В общем, можно это делать с помощью функции style. Если стили где-то храним, то делаем примерно такое:

      private fun editTextStyle(editText: EditText) {
          editText.textSize = 18f
          editText.textColor = Color.RED
      }
      
      //=>
      
      relativeLayout {
          editText {
              style { editTextStyle(this) }
          }.layoutParams { centerInParent(); }
      }
      


      2) Custom view — легко, нужно просто знать. Пишем такую функцию (допустим, класс называется MyView):
      fun ViewManager.myView(init: MyView.() -> Unit = {}) =
              __dslAddView({ MyView(it) }, init, this)
      

      И можем спокойно использовать:
      relativeLayout {
          myView {
              //...
          }
      }
      


      3) Еще проще:
      val size = dip(getResources().getDimension(R.dimen.my_dimen))
      


      4) Тоже достаточно просто, создаем две разметки и в рантайме проверяем, какая ориентация:
      private fun portrait() {
          linearLayout {
          }
      }
          
      private fun landscape() {
          relativeLayout {
          }
      }
      
      //=>
      
      if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
          landscape()
      }
      else {
          portrait()
      }
      


      1. stepango
        27.05.2015 10:28
        +1

        Спасибо! Ребота со стилями и dimen не выглядит очень удобной, учитывая что использовать сушествующие стили например из Sapport Library — совсем не тривиально. На мой взгляд весь смысл описания UI в XML это отделение разметки от кода, а в Anko объединение разметки и кода описывается как приемущество что довольно странно на мой взгляд. И еще один вопрос у меня назрел. Как в Anko можно переиспользовать разметку(аналоги layout и merge тегов)?


        1. Arturka Автор
          27.05.2015 10:40

          Да, есть такой момент. Впрочем, все это может быть переработано еще)
          Насчет переиспользования есть кое-что — сам не пробовал, но похоже. Это с xml, а с DSL — создаем функции, которые будут возвращать часть UI (например, поле ввода с кнопкой) один раз и используем везде.


      1. xGromMx
        27.05.2015 12:18

        Автор Anko работает над удобным плагином для DSL и IDEA github.com/yanex/dsl-preview. Также у него есть стартеркит для Anko github.com/yanex/anko-template-project