После верстки мы изучили немного фундаментала по Android проекту, настало время научиться настраивать взаимодействие кода файла MainActivity.kt с макетом экрана.
Атрибут id
Окей. У нас есть некоторые элементы в макете, которые должны обрабатывать различные действия. Но как с ними взаимодействовать? Для этого на View вешается дополнительный атрибут — id, который мы уже использовали ранее.
android:id="@+id/tvQuestionWord"
Разберем подробнее этот атрибут. Обратите внимание, перед словом id поставлен знак “плюс”. Знак сообщает системе, что этот идентификатор новый и его необходимо создать, а не обратиться к существующему. Плюс проставляется автоматически, поэтому нет необходимости держать это в голове, но знать об этом рекомендуется.
Стиль названия id’шников
Далее про нейминг “айдишников”. В сообществе принято называть идентификаторы в стиле camelCase (начиная с маленькой буквы и каждое следующее слово с заглавной). Дополнительно я рекомендую вначале обозначать тип view в сокращенном виде — от первых слов названия view. Например: если это TextView — tv, если это Button — btn, если это EditText (”вью” для ввода текста от пользователя) — et. Это поможет ориентироваться в элементах, когда вы будете работать с ними в коде. Допустим, если вам будет нужна только кнопка — вы начинаете вводить btn и дропдаун с вариантами покажет только кнопки. Стиль можете подобрать свой, как всегда, главное, чтобы он был единым для всего проекта.
И еще одна особенность про атрибут id. Если у элемента установлен этот атрибут, система Android может использовать этот идентификатор для сохранения и восстановления состояния элемента при пересоздании Активити или Фрагмента. Мы подробно коснемся этого чуть позже, пока только в двух словах приведу пример. При изменении ориентации экрана или при смене языка — Activity, как экземпляр, пересоздается в системе. И, допустим, если вы вводили какой-либо текст в соответствующий элемент “вью”, он может не сохраниться при вышеописанных обстоятельствах. Наличие атрибута id решает эту проблему.
Получение id элемента View в Activity
Теперь зададимся вопросом “как получить элемент “вью” по его id в Activity?”. Переместимся в Activity. Писать код будем в методе onCreate(). Как я упоминал ранее, метод вызывается при создании экземпляра Activity, то есть можно считать что в момент создания нашего основного экрана. Этот метод является частью так называемого жизненного цикла Activity. На самом деле там есть еще ряд методов, которые запускаются перед тем, как пользователь увидит содержимое экрана. И всех их можно переопределять и добавлять нужные действия в зависимости от потребностей. Их нужно будет знать (один из самых популярных вопросов на собеседовании). Разберем в отдельном уроке про Activity.
Способы получения id из XML разметки
Есть несколько способов получения id. Перечислю 2 самых популярных и актуальных.
- findViewById
- ViewBinding
findViewById
Теперь подробнее. Первый способ через findViewById является классическим. Давайте создадим переменную, в которую поместим ссылку на элемент макета. Этот способ требует явного указания типа элемента “вью”. Искать будем по ранее созданному id, обращаясь к классу R, который и хранит сгенерированные “айдишники”.
val tvQuestionWord: TextView = findViewById(R.id.tvQuestionWord)
Теперь зададим TextView какой-нибудь атрибут прямо из кода, чтобы проверить работоспособность. Текстовому элементу, например, мы можем задать новый текст и поменять цвет. Текст задается через property (или свойство) text. Мы просто присваиваем ему новое значение. С цветами есть варианты. Но все их объединяет вызов функции установки цвета для “вьюхи” setTextColor().
tvHello.setTextColor(Color.*GRAY*)
— вот так мы передаем в функцию цвет (а точнее ссылку на объект цвета) из стандартной библиотеки Android без дополнительных манипуляций.
tvHello.setTextColor(Color.parseColor("#AA0AEF"))
— здесь в метод передается результат выполнения другой функции parseColor(), которая распознает Hex-код цвета.tvHello.setTextColor(ContextCompat.getColor(this, R.color.*customTextColor*))
— в этом случае мы реализовали передачу цвета из ресурсов. Этим способом будем пользоваться наиболее часто, запишите его конструкцию в конспект.
Коротко о том, что тут происходит. ContextCompat — класс библиотеки поддержки Android, который обеспечивает обратную совместимость с различными системными функциями, в том числе и для доступа к ресурсам. В метод getColor() помимо ссылки на наш кастомный цвет, мы передаем context текущей Activity. this — конкретно указывает, что это контекст текущей Activity. Контекст требуется, так как у Активити может быть своя тема, переопределяющая текст. Не пугайтесь, что сейчас это кажется непонятным, позже мы еще вернемся к этим темам.
ViewBinding
И следующий тип о котором я хотел бы рассказать — ViewBinding. Наиболее актуальный сейчас в мире XML верстки способ для использования. Он принципиально отличается от первого способа технически. View Binding генерирует привязку для каждого файла разметки в проекте, что позволяет получить доступ к элементам разметки через этот объект. Из преимуществ — объект создается только один раз при создании Activity. В этом случае уже не нужно вызывать findViewById для каждого элемента, так как это становится проблемой, когда элементов много.
Но чтобы начать пользоваться им, необходимо произвести некоторые настройки в проекте. А именно включить его в конфигурационном gradle файле проекта и создать экземпляр binding класса в Activity.
Активация ViewBinding в Android проекте
Сначала включаем в проекте. Для этого зайдите в файл build.gradle.kts в модуле проекта, для которого необходимо активировать ViewBinding. Напомню сейчас у нас приложение состоит из единственного модуля app. В иерархии файлов модуль отображается со значком квадрата. Далее пропишите конфиг, который и включает возможность использования ViewBinding.
Не забудьте синхронизировать проект.
buildFeatures {
viewBinding = true
}
После синхронизации под капотом создаются классы для каждого файла XML разметки с суффиксом Binding. То есть если макет называется activity_main.xml, то класс для этой разметки будет называться ActivityMainBinding. Сейчас все будет понятно.
Инициализация ViewBinding в Activity
Идем в MainActivity. Что здесь нужно сделать, объясняю по пунктам:
Первое. Объявить переменную binding для хранения экземпляра сгенерированного binding класса.
private lateinit var binding: ActivityMainBinding
Я объявляю lateinit var, что означает, что эта переменная с отложенной инициализацией. Но, забегая вперед, я не рекомендую использовать в проекте lateinit без крайней необходимости. Если в двух словах описать причину — в ситуации, если компилятор попытается обратиться к этой переменно до того, как она будет проинициализирована, приложение упадет с ошибкой, причину которой выяснить бывает не просто. Поэтому чуть позже в этом уроке мы поменяем подход к созданию переменной для ActivityMainBinding.
Второе. В onCreate() в созданную переменную сохраняем экземпляр привязки. И у класса разметки вызываем inflate() (переводится, как “раздувать”) с параметром layoutInflater — он создает экземпляр binding класса и связывает его с разметкой Activity. Результатом этого вызова будет экземпляр класса ActivityMainBinding, который содержит ссылки на все “вьюхи”, объявленные в разметке. Мы как бы “раздули” разметку. То есть из XML файла разметки создали объекты View.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityLearnWordBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLearnWordBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
- Теперь через эту переменную можно обращаться к элементам разметки. Но завершающая часть настройки — это получить корневой элемент разметки, который по умолчанию называется root. И передаем этот корневой элемент в setContentView().
Обращение к элементу разметки через ViewBinding
Настроили. Теперь чтобы обратиться к элементу разметки по id, мы всегда обращаемся к экземпляру binding. Закомментируем предыдущий код, который уже не актуален и реализуем все то же самое через ViewBinding. Кстати, если кликать на название класса или на элемент view с зажатым ctrl или cmd — то можно удобно навигироваться к файлу разметки и к искомому элементу в частности.
В текущей реализации мы обращаемся всего к одному элементу разметки. Добавим еще какой-нибудь “айдишник” и представим, что таких “айдишников” не 2, а 20. Чтобы каждый раз не прописывать обращение binding к отдельному элементу, можно объединить их с помощью конструкции with {}.
with(binding) {
tvHelloWorld.text
tvHelloWorld2.text
}
Таким образом внутри этих фигурных скобок можно сразу прописывать id элемента (или если сказать точнее — поле сгенерированного класса ActivityMainBinding).
Backing property (резервное свойство)
Добавим еще один штрих, чтобы получилось красиво. Есть такая штука — backing property (резервное свойство). Это скрытое свойство, которое используется для хранения другого свойства в классе. Довольно популярная концепция, покажу на примере переменной binding. Суть такая: создаем две переменные. Одну для использования ее в классе, другую только для инициализации ее соответствующим значением.
Вот как это выглядит в нашем примере. Сначала объявим изменяемую переменную с нуллябельным типом и проинициализируем ее null. В названии прописываем в начале нижнее подчеркивание, чтобы их различать.
Далее объявим неизменяемую переменную с нормальным названием и инициализируем ее объявленной выше переменной. Именно к этой переменной будем обращаться, чтобы получить айдишники разных вью в макете.
И вроде бы все ок, но теперь переменная binding (без подчеркивания) имеет нуллябельный тип. И это не то, что нам нужно. Нужна гарантия того, что при обращении к элементу файла разметки класс ActivityMainBinding не будет равен null или по крайней мере мы должны знать, что что-то пошло не так. Можно каждый раз проставлять оператор безопасного вызова, но это плохая стратегия. Обработаем сразу.
Поэтому кастомизируем геттер. Если переменная binding не null, все окей, при обращении отдаем проинициализированное значение. В противном случае выбрасываем IllegalStateException с информационным сообщением. Теперь, если возникнет ситуация, когда обращение к переменной binding случится раньше, чем она будет проинициализирована в методе onCreate, то приложение упадет с ошибкой. Но вы будете знать в каком месте и по какой причине ошибка. Это особенно актуально, когда у вас большой проект с большим количеством Активити и Фрагментов.
private var _binding: ActivityMainBinding? = null
private val binding
get() = _binding ?: throw IllegalStateException("Binding for ActivityMainBinding must not be null")
Про backing property можно сказать так — это обеспечивает безопасность при обращении к свойству binding снаружи класса, так как оно всегда будет иметь ненулевое значение.
Дальше начинаем пилить интерактив для экрана. Будем вешать замоканные (или захардкоженные) события на элементы и окрашивать контейнеры в зависимости от правильности ответа.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика