Чтобы выполнить операцию над одним из представлений при работе с 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