Android Interview Review
3.54K members
233 links
Популярные вопросы и ответы с собеседований на Android-разработчика.

Канал для Java-разработчиков @JavaSobes

Связаться с нами @SobesAdmin

https://itsobes.ru
Download Telegram
to view and join the conversation
В чем недостатки использования библиотеки Data Binding?

С самого создания Data Binding библиотека подвергалась критике. Некоторые проблемы были решены, но у библиотеки до сих пор плохая репутация.
Разберем недостатки, которые существуют и по сей день:

1) Код в XML
Это основной минус, который является главной фичей. Написание кода в XML плохо тем, что код не тестируем, его неудобно искать и читать, логика перемещается в верстку.
Пример злоупотребления кодом в XML:

android:visibility="@{price > 500 ? View.GONE : View.VISIBLE}"


Хорошей практикой считается не писать логику в лэйаутах, а подписываться на обновления полей ViewModel:

android:visibility="@{viewModel.priceVisibility}"


Код ViewModel:

val priceVisibility = ObservableInt(GONE)

fun updatePrice(price: Int) {
if (price > 500) {
priceVisibility.set(GONE)
} else {
priceVisibility.set(VISIBLE)
}
}


При такой реализации код обновления visibility можно протестировать, но в этом случае можно просто подписаться на обновления поля priceVisibility в Активити или Фрагменте, которые работают с ViewModel и обновлять visibility в коде.

2) Генерация кода
Data Binding генерирует код для связывания XML c data-классами. Кодогенерация иногда ломает инкрементальную сборку, особенно в больших проектах. В этом случае при изменении классов, использующих Data Binding, приходится делать clean build.
Также кодогенерация в целом увеличивает время билда.

3) Проблемы с тулингом
Команда гугла постоянно улучшает поддержку Data Binding в Android Studio. Но бывают случаи, когда ссылки или импорты, сгенерированные библиотекой перестают распознаваться в IDE. Это является частой проблемой больших проектов, использующих DataBinding.

Вывод
Data Binding может быть полезен для маленьких проектов и прототипов, т.к. увеличивает скорость написания кода.
Для больших проектов Data Binding создает больше проблем, чем приносит пользы.
Правильное использование Data Binding ненамного уменьшает количество кода.

Дополнительные материалы: [1], [2], [3].

#Jetpack
В чем разница между Doze Mode и App Standby?

Doze и App Standby введены в Android 6.0 (API level 23). Это функции ОС, которые ограничивают работу приложений в бэкграунде для сохранения заряда батарейки.

Основная разница в том, что Doze Mode – это режим операционной системы, который применяется на все установленные приложения, а App Standby – это состояние конкретного приложения.

Система входит в Doze Mode, когда девайс не используется и не стоит на зарядке. Система периодически выходит из Doze для выполнения отложенных фоновых задач.

Приложение, которое находится в фоне определенный период времени, входит в режим App Standby. Когда девайс ставится на зарядку все приложения выходят из состояния Standby.

Ограничения этих режимов описаны в документации.

#System
Что такое Spannable?

Spannable – это интерфейс, характеризующий текст, который имеет стилистическую разметку. 
Например с помощью Spannable можно создать текст, часть которого окрашена в иной цвет.

Метод Spannable.setSpan() принимает произвольный объект типа Object, который используется для разметки. Этот метод не бросает исключений. Классы, которые реализуют интерфейс Spannable, должны игнорировать неподдерживаемые объекты разметки.

ForegroundColorSpan – один из примеров класса-разметки. Этот класс используется для изменения цвета текста. 

Документация по Spannable

#View
Для чего нужен метод Fragment.setRetainInstance()?

Метод setRetainInstance() принимает boolean параметр. По умолчанию значение retainInstance фрагмента – false
Если retainInstance выставлен в true, то фрагмент переживает пересоздание хост-активити, например при повороте экрана.

Когда активити пересоздается, фрагмент с retainInstance=true отсоединяется от старой активити и присоединяется к новой. Поэтому при пересоздании активити у фрагмента не вызываются методы onDestroy() и onCreate(), но вызываются onDetach(), onAttach() и onActivityCreated().

setRetainInstance() может быть использован только на фрагментах, не добавленных в backstack.

#Fragment
Что такое LiveData?

LiveData – это класс, который хранит данные и реализует паттерн Observable. LiveData входит в Android Jetpack.
Преимущество LiveData перед другими реализациями шаблона Observable в том, что LiveData заточена на работу с жизненными циклами Android компонентов, таких как Activity, Fragment и Service.
Для подписки на обновления LiveData используется метод observe(), который принимает объект типа LifecycleOwner и функциональный интерфейс Observer.
Интерфейс LifecycleOwner реализуется классами Android компонентов, например AppCompatActivity, LifecycleService, Fragment.

LiveData доставляет данные только наблюдателям, которые находятся в активном состоянии. Observer входит в активное состояние, когда соответствующий ему lifecycle переходит в состояние STARTED или RESUMED.

LiveData автоматически отписывает наблюдателей, когда их lifecycle переходит в состояние DESTROYED.

#Jetpack
В чем разница между LiveData.observe() и LiveData.observeForever()?

Метод LiveData.observe() принимает два параметра: lifecycleOwner и observer. Вся функциональность, описанная в предыдущем посте, предоставляется через этот метод. 
LiveData следит за жизненным циклом lifecycleOwner, доставляет ивенты только активным подписчикам и удаляет подписчиков, которые перешли в состояние DESTROYED.

Метод LiveData.observeForever() принимает один параметр: observer. Этот метод реализует классическую схему шаблона Observer-Observable. Ивенты доставляются подписчику всегда, незавимо от его состояния.
Также существует симметричный метод LiveData.removeObserver(), созданный для ручного удаления подписчиков.
Обычно метод observeForever() используется в тестах, а не в коде приложения. 

#Jetpack
Как правильно подписываться на LiveData во фрагментах?

Класс Fragment реализует интерфейс LifecycleOwner, поэтому в метод LiveData.observe() первым параметром можно передавать this, также как в Activity.

Но жизненный цикл View отличается от жизненного цикла фрагмента. Ивент из LiveData может прийти после вызова метода onDestroyView(). В этом случае View фрагмента занулится, и при попытке обновления UI будет брошен NullPointerException.

Поэтому рекомендуется использовать метод getViewLifecycleOwner(), который возвращает объект LifecycleOwner, ассоциированный с жизненным циклом View.
В этом случае подписываться необходимо в методе onCreateView(), а после вызова onDestroyView() подписчик перейдет в состояние DESTROYED и автоматически отпишется от обновлений LiveData.

#Jetpack
#Fragment
Какие трансформации возможны на LiveData?

Трансформации LiveData производятся утилитным классом Transformations.
Этот класс имеет три статических функции:

map() преобразует каждый элемент LiveData. Аналогичен функции map на списках и в RxJava.

switchMap() преобразует каждый элемент исходной LiveData в новый промежуточный LiveData-стрим. Аналогичен switchMap() в RxJava.

distinctUntilChanged() не принимает параметров и возвращает новый объект LiveData, который уведомляет об обновлении только если следующий элемент не равен предыдущему. Для сравнения используется equals().

Помимо использования статических методов класса Transformations, можно подключить KTX расширения библиотеки LiveData и получить методы в качестве extension-функций.

Примеры использования на картинке.

#Jetpack
Как объединить несколько LiveData?

Для объединения нескольких LiveData используется класс MediatorLiveData.
Метод MediatorLiveData.addSource() принимает два параметра: исходный LiveData и функциональный интерфейс Observer, который вызывается при обновлении исходного LiveData.

Для объединения двух LiveData нужно добавить исходные LiveData методом addSource(), и на каждый ивент обновлять значение в MediatorLiveData.
Пример на картинке.

#Jetpack
Как создать кастомный transform-оператор LiveData?

Кастомный transform-оператор для LiveData создается следующим образом:

1. Объявляется extension-функция на классе LiveData, которая принимает функциональный тип как параметр.

2. Создается MediatorLiveData.

3. На MediatorLiveData вызывается метод addSource(). В этот метод первым аргументом передается LiveData, на который вызывается transform-оператор (т.е. this).

4. Вторым аргументом реализуется логика transform-оператора, с использованием функции, переданной в качестве параметра.

На картинке пример реализации оператора filter.

#Jetpack
Для чего нужен класс ViewModel из Architecture Components?

ViewModel – абстрактный класс, упрощающий реализацию паттерна MVVM в Android-приложении.

В общем случае ViewModel используется следующим образом:

1. Создается наследник класса ViewModel, который реализует VM-логику в MVVM компоненте.

2. Объект ViewModel инстанциируется во View-слое (активити или фрагменте) с помощью delegated property viewModels():

val viewModel: MyViewModel by viewModels()

3. Активити или фрагмент использует объект viewModel для получения обновлений UI.

Объект, созданный с помощью by viewModels() переживает пересоздание активити и фрагмента при изменении конфигурации (например при повороте экрана).

Объект ViewModel уничтожается только в случае, если соответсвующий View-класс не пересоздается. Для активити это происходит, когда вызывается onDestroy() после вызова метода finish(). В случае фрагмента – после вызова onDetach().

При уничтожении объекта ViewModel вызывается метод onCleared().

#Jetpack
Является ли ViewModel заменой onSaveInstanceState()?

– Нет. ViewModel и onSaveInstanceState() используются в разных ситуациях.

onSaveInstanceState() предназначен для сохранения небольшого количества данных, которые позволяют восстановить состояние UI в следующих случаях:

• Остановка процесса приложения для восстановления ресурсов памяти;

• Изменение конфигурации.

Не следует использовать onSaveInstanceState() для сохранения больших массивов данных.

ViewModel переживает только изменение конфигурации, но уничтожается при остановке процесса. Это делает ViewModel менее универсальным механизмом, чем onSaveInstanceState(), но позволяет сохранять большие объекты во время изменения конфигурации и пересоздания активити.

#Jetpack
#State
Где хранятся данные onSaveInstanceState()?

Метод onSaveInstanceState() используется для сохранения состояния активити при изменении конфигурации.
onSaveInstanceState() сохраняет данные в объекте Bundle, который реализует ассоциативный массив (хранилище вида ключ-значение).

Ранее мы писали, что данные, сохраненные через onSaveInstanceState(), переживают остановку процесса приложения. Это значит, что Bundle хранится не в памяти приложения.

Также известно, что onSaveInstanceState() не сохраняет данные на диске. Это можно понять потому, что состояния приложений не могут быть восстановлены после перезагрузки девайса. Кроме того документация класса Parcel, который используется для сохранения Parcelable объектов в Bundle, не рекомендует использовать Parcel для persistent storage.

Где же сохраняется Bundle из onSaveInstanceState()?

Ответ: в системном классе ActivityManagerService, о котором мы уже писали в посте о запуске приложения.

Для каждой запущенной активити создается инстанс класса ActivityRecord. Этот класс имеет поле icicle типа Bundle. Именно в это поле сохраняется состояние после вызова onSaveInstanceState().

При остановке активити Bundle отправляется в системный процесс через Bindler IPC. Далее на классе ActivityManagerService вызывается метод activityStopped(), который принимает объект Bundle. Этот метод находит ActivityRecord, соответствующий остановленной активити и записывает полученный Bundle в поле icicle класса ActivityRecord.

Данная реализация не задокументирована и может быть изменена в будущих версиях Android.

#OS
#State
В чем разница между Assets и Raw-ресурсами?

Assets и Raw-ресурсы – это механизмы, которые позволяют добавить дополнительные файлы произвольного формата в Android-приложение.

Для использования Assets создается директория src/main/assets. Эта директория может содержать произвольные файлы и поддиректории.
Для получения контента asset-файла используется метод Context.getAssets(), который возвращает объект класса AssetManager. Далее вызывается метод AssetManager.open(), который принимает имя файла и возвращает InputStream.
Если файла с заданным именем не существует, то метод open() бросает IOException.

Для использования Raw-ресурсов создается resource-директория res/raw. Эта директория подчиняется тем же правилам, что и другие ресурсы. Она может иметь квалификаторы (например -ru), в ней нельзя создавать вложенные директории, а имена файлов должны быть в нижнем регистре.
Во время компиляции для каждого raw-файла создается id в классе R. Для файла sound.mp3 будет создан id R.raw.sound.
Для доступа к raw-ресурсу используется метод Resources.openRawResource(), который принимает id ресурса и возвращает InputStream.

Доступ к raw-ресурсам более эффективен и безопасен за счет статической генерации id. Также с помощью квалификаторов возможно использование разных файлов для различных конфигураций системы.
С другой стороны механизм Assets более гибкий, т.к. позволяет получать полное имя файла и создавать вложенные директории.

#Resources
Можно ли создавать ресурсы с несколькими квалификаторами?

– Да.

Но существуют ограничения:

1. Квалификаторы должны быть расположены в порядке приоритета. Приоритет квалификаторов соответствует порядку в таблице.
Неверный вариант: drawable-mdpi-ru.
Верный вариант: drawable-ru-mdpi.

2. Невозможно применить несколько квалификаторов из одной категории, например values-ru-be/. Ресурс придется продублировать в двух директориях: values-ru/, values-be/

#Resources
Шпаргалка по асимптотической сложности алгоритмов:

https://www.bigocheatsheet.com/

#пятница
По какому алгоритму разрешаются квалификаторы ресурсов?

Квалификаторы ресурсов разрешаются по следующему алгоритму:

1. Исключаются все ресурсы, которые противоречат текущей конфигурации. Исключением являются квалификаторы, определяющие плотность экрана (dpi). Для плотности экрана выбирается не точно соответствующий ресурс, а наиболее подходящий.

2. Выбирается следующий квалификатор, в соответствии с порядком в таблице.

3. Если существует директория ресурсов с данным квалификатором, то переходим на шаг 4, иначе возвращаемся на шаг 2.

4. Исключаются все директории ресурсов, которые не содержат данный квалификатор.
В случае dpi квалификатора исключаются директории, которые не являются наиболее подходящими к конфигурации экрана.

5. Повторить шаги 2, 3 и 4 пока не останется единственная подходящая директория ресурсов.

Пример использования алгоритма в документации.

#Resources