Kotlin для Android

    Наверное, из тех, кто интересуется разработкой под 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.
    P.P.S. Хотите пообщаться с другими разработчиками, интересующимися Kotlin? Добро пожаловать в Gitter.
    Поделиться публикацией

    Комментарии 22

      0
      Слежу за этим проектом с раннего этапа, еще когда он именовался koan) Имел каверзную дисскусию с автором, было интересно.
        –3
        И опять какая-то незримая фея сыплет пыльцу, на мою карму :)
          +6
          держите нас в курсе ;-)
        0
        Насчет последнего, или пример не удачный но мне не кажется что IllegalStateException будет чем-то принципиально отличаться от NPE. Может этот делагат можно использовать как монаду?
          0
          Вы не совсем правильно поняли. Конструкция
          by Delegates.notNull()
          нужна чтобы поле mHelloWorldTextView имело тип TextView, а не TextView?, чтобы не было всяких операторов типа "?." и "!!.".

          Для справки (на всякий случай) — объект типа TextView не может принимать значение null. Для null есть типы с "?" в конце — TextView?.. Это во всем котлине. Если объект имеет тип "?", то к его полям нельзя обращаться непосредственно через "." — код просто не скомпилируется. Для этого есть операторы "?." и "!!." (сложно представить, как можно было додуматься до такого оператора:))
            +1
            Я к тому что по моему опыту textView в Activity, в 90% случаев имеет null значение не потому что ему намерено null присвоили, а потому что забыли инициализировать в onCreate(). И тут Delegates.notNull() ни разу не помощник получается. Потому и говорю про не самый удачный пример.
              0
              Опять-таки дело не в этом. Я не зря привел такой пример:
              private var mHelloWorldTextView : TextView = //???
              

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

              Впрочем, Kotlin Android Extensions решает все проблемы.
          +2
          Гораздо лучше сделать вот так:

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

          Во фрагментах так просто не получится, но в активити самое то.
            +2
            Вообще еще больше мне импонирует kotlinlang.org/docs/tutorials/android-plugin.html которые позволяет вообще избавиться от findViewById
            0
            К сожалению, подход не универсальный и не приводит у уменьшению кода, по факту это тоже самое, что и установить поле руками (ну возможна небольшая экономия на ленивой инициализации).
            0
            А что как kotlin для андроид сосуществует с DI типа roboguice или dagger? Слышал что теряется совместимость. Может есть какие-то свои альтернативы?
            +1
            >из тех, кто интересуется разработкой под Android, только ленивый не слышал про Kotlin
            О, наконец-то я достоверно выяснил, что я ленив :)
              +1
              День открытий (-:
              Я тоже не слышал (-:
              +2
              У меня несколько вопросов по Anko DSL.
              Как применять стили?
              Как использовать custom views?
              Есть ли удобный способ брать размеры элеметов из dimen.xml?
              Что делать если понадобится 2 разметки для portrait и landscape?
                0
                Хорошие вопросы)
                Во-первых, замечу еще такую вещь, что при открытии файла 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
                  Спасибо! Ребота со стилями и dimen не выглядит очень удобной, учитывая что использовать сушествующие стили например из Sapport Library — совсем не тривиально. На мой взгляд весь смысл описания UI в XML это отделение разметки от кода, а в Anko объединение разметки и кода описывается как приемущество что довольно странно на мой взгляд. И еще один вопрос у меня назрел. Как в Anko можно переиспользовать разметку(аналоги layout и merge тегов)?
                    0
                    Да, есть такой момент. Впрочем, все это может быть переработано еще)
                    Насчет переиспользования есть кое-что — сам не пробовал, но похоже. Это с xml, а с DSL — создаем функции, которые будут возвращать часть UI (например, поле ввода с кнопкой) один раз и используем везде.
                    0
                    Автор Anko работает над удобным плагином для DSL и IDEA github.com/yanex/dsl-preview. Также у него есть стартеркит для Anko github.com/yanex/anko-template-project
                      0
                      1) Можно так подсунуть

                          <style name="TxtStyle" parent="Theme.AppCompat.Light">
                              <item name="android:textSize">18dp</item>
                              <item name="android:textColor">#ff0000</item>
                          </style>

                      relativeLayout {
                          ankoView({ TextView(ContextThemeWrapper(ctx, R.style.TxtStyle)) }) {
                      
                          }.layoutParams { centerInParent(); }
                      }

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое