
Чтобы выполнить операцию над одним из представлений при работе с 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.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming