Чтобы выполнить операцию над одним из представлений при работе с UI-слоем приложения Android, его нужно получить его через findViewById. Несмотря на то, что использование API может показаться простым, он представляет собой шаблон для Activities. Помимо этого, код для связывания всех представлений обычно заканчивается в onCreate, полностью отделенный от свойств представлений. ?

Рассмотрим несколько подходов, а затем разберемся, как использовать отложенную инициализацию (lazy initialisation) в Kotlin для решения этой проблемы.

Положение дел в Java

Для начала рассмотрим, как эти задачи решаются в Java. Для связывания представлений с полем используется findViewById, приводящее полученное представление к правильному подклассу. Для activity представления обычно связаны в onCreate, однако это можно выполнить где угодно, если представления получены в другое время.

public class PlanningActivity extends AppCompatActivity {
private TextView planningText;
private ImageView appIcon;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_planning);
// Before: Needed to cast
planningText = (TextView) findViewById(R.id.planning_text);
// Now: No longer need to
appIcon = findViewById(R.id.app_icon);

planningText.setText("Hello!");
}
}

Приведенный пример содержит всего два представления, в то время как в обычном случае доступ нужно получить ко множеству представлений. Чтобы избежать этого повторяющегося шаблона Jake Wharton разработал Butter Knife, который использует обработку аннотаций для упрощения связывания представлений. ?

public class PlanningActivity extends AppCompatActivity {
@BindView(R.id.planning_text) TextView planningText;
@BindView(R.id.app_icon) ImageView appIcon;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_planning);
ButterKnife.bind(this);
planningText.setText("Hello!");
}
}

Применение Kotlin

Kotlin обладает множеством новых функций и может предложить лучшие решения для старых задач. К примеру, Kotlin Android Extensions — плагин для языка Kotlin. Он автоматически генерирует свойства, соответствующие ID представлений, в файле layout. Благодаря этому, представления не нужно сохранять вручную в качестве свойств, поскольку они доступны через синтетические свойства, сгенерированные плагином.

import kotlinx.android.synthetic.main.activity_planning.*

class PlanningActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.activity_planning)
planning_text.text = "Hello"
}
}

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

Lazy binding

Благодаря делегированным свойствам Kotlin геттер и сеттер делегируются при инициализации свойств, предоставляя возможность отложенной инициализации. Идея заключается в том, что при первом получении доступа функция используется для инициализации свойства, а при дальнейших обращениях она просто возвращается. Идеальное решение для связывания представлений, поскольку при первом получении доступа будет вызвана findViewById, а при последующих вызовах будет возвращено сохраненное представление. Следует отметить, что на данный момент мы применяем эту технику к Activity

Делегированные свойства применяются через ключевое слово by с lazy в качестве глобальной функции и лямбда-аргументом, что значительно упрощает применение.

private val planningText by lazy {
findViewById<TextView>(R.id.planning_text)
}

С помощью проверки сигнатуры узнаем, что lazy также используется с аргументом LazyThreadSafetyMode, который синхронизирует доступ к свойству lazy по умолчанию, обеспечивая безопасность потока. Поскольку связанные с помощью lazy представления затрагиваются только основным потоком, можно оптимизировать производительность и отключить это поведение.

private val planningText by lazy(LazyThreadSafetyMode.NONE) {
findViewById<TextView>(R.id.planning_text)
}

При применении этого решения в приложении, один и то же вызов lazy можно совершить множество раз. Благодаря этому, его можно извлечь в глобальную функцию, сохранив чистоту кода.

// LazyExt.kt
fun <T> lazyUnsychronized(initializer: () -> T): Lazy<T> =
lazy(LazyThreadSafetyMode.NONE, initializer)

// PlanningActivity.kt
private val planningText by lazyUnsychronized {
findViewById<TextView>(R.id.planning_text)
}

Bind view

Единственная часть блока lazy findViewById, которая изменяется при каждом использовании, это обобщенный тип (generic type). Благодаря функции расширения Activity весь блок может использоваться повторно при каждом связывании представлений. 

fun <ViewT : View> Activity.bindView(@IdRes idRes: Int): Lazy<ViewT> {
return lazyUnsychronized {
findViewById<ViewT>(idRes)
}
}

Рассмотрим еще одну реализацию связывания представлений с помощью lazy. Тип представления можно указать в свойстве или в качестве общего ограничения для bindView. Все зависит от личных предпочтений или специфики самого проекта.

class PlanningActivity : AppCompatActivity() {
  private val planningText by bindView<TextView>(R.id.planning_text)
  // or
  private val planningText: TextView by bindView(R.id.planning_text)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_planning)
    planningText.text = "Hello!"
  }
}

Благодаря этим действиям, мы получаем краткий и чистый код с полностью контролируемым связыванием представлений. Поскольку функция bindView является упаковщиком для findViewById, аналогичное расширение можно применить для связывания представлений в любом месте базы кода в рамках Activities. ?

Другие случаи использования

Технику связывания представлений с использованием lazy можно с легкостью применить, к примеру, при создании ViewHolder, для использования в RecyclerView. Можно создать схожее расширение для ViewHolder, чтобы получить доступ к тому же API, как и в Activities.

Проблемы при применении этой техники могут возникнуть с Fragment из-за различий в жизненном цикле в сравнении с Activities и Views. Если применять ее тем же образом, то проблема возникнет, когда свойство lazy ссылается на предыдущий экземпляр представления после воссоздания представления Fragment. В качестве решения можно назначить представления в onCreateView и избегать использования свойств lazy с Fragments.

Также можно использовать жизненный цикл из Architecture Components для устранения проблемы. Создав собственную версию Lazy, можно сбросить сохраненное значение в onDestroyView, вызвав findViewById при следующем доступе. Пример реализации этой идеи можно найти в демо коде для этой статьи. ?

Заключение

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

Спасибо за чтение и счастливого программирования! ?

Демо код для этой статьи на GitHub.


Перевод статьи Andrew Lord: Using lazy in Kotlin to bind Android views

Предыдущая статья10 лайфхаков для работы с библиотекой Pandas
Следующая статьяЗаставляем глубокие нейронные сети рисовать, чтобы понять, как они работают