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

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

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

https://itsobes.ru
Download Telegram
to view and join the conversation
Что такое MVP?

MVP (Model-View-Presenter) – это шаблон проектирования, который используется для разработки пользовательского интерфейса в приложении.
MVP разделяет ответственность за бизнес логику и логику отображения.

MVP состоит из следующих частей:

• Model представляет собой данные, которые необходимо показать пользователю. В большинстве Android-приложений моделью выступает слой, отвечающий за получение данных с бэкэнда.

• View – это класс, отвечающий за отображение данных. В Android-приложениях View – это обычно Activity или Fragment.
Кроме того View слушает пользовательские ивенты и делегирует их обработку в Presenter.
Например View может иметь такой код:
loginButton.setOnClickListener { presenter.onLoginClicked() }


• Presenter – это класс, который имеет ссылки и на View, и на Model, и расположен между ними. Presenter отвечает за обработку ивентов, приходящих из View, получение данных из Model и обновление View c полученными данными.
В Android-приложениях хорошей практикой считается делать Presenter независимым от Android SDK. Другими словами Presenter не имеет доступа к Android классам напрямую и может быть использован в plain java приложении.

Подробнее о MVP

#Architecture
Назовите минусы MVP (1/2)

Проблемы классической реализации MVP в Android-приложении:

• Циклическая зависимость между View и Presenter

В MVP View хранит ссылку на Presenter, а Presenter ссылается на View. Это не так страшно, т.к. Presenter не знает о конкретном классе, реализующем интерфейс View, но факт остается фактом.
Из-за того, что Presenter ссылается на View, время его жизни должно не превышать время жизни View. Presenter обязан остановить все асинхронные операции, до перехода Activity или Fragment в состояние destroyed.
Это приводит нас ко второй проблеме.

• Presenter предоставляет методы для обработки жизненного цикла View

В хорошей реализации MVP Presenter не имеет методов жизненного цикла, таких как onStart()/onStop(). Вместо этого создаются методы типа init() для загрузки и отображения данных и unsubscribe() для отписки от асинхронных вызовов.
При такой реализации Presenter не знает о жизненном цикле, но View должна сама заботиться о вызове этих методов. Если View не вызовет unsubscribe() до отработки метода onDestroy(), то это может привести к утечке Activity.

#Architecture
Назовите минусы MVP (2/2)

Продолжим список классических проблем MVP:

• Раздутый интерфейс View
Если экран имеет много элементов и состояний, это приводит к тому, что в интерфейс View добавляется слишком много методов. Presenter будет использовать каждый метод для обновления маленькой части UI экрана.
Эта проблема решается разбиением экрана на несколько MVP компонентов, но иногда сложно найти правильный trade off.

• Сложно сохранять состояние
В правильной реализации MVP состояние хранится в Model. Пользователь делает действие → Presenter получает обновленную Model → Presenter обновляет View. Пользователь поворачивает экран → View пересоздается → Presenter инициализируется → Model возвращает закэшированные данные → Presenter обновляет View в том же состоянии.

В реальности помимо состояния бизнес модели существует состояние View. Например текст, введенный в EditText, но не отправленный на бэкенд, или состояние кастомной View (элемента UI, а не интерфейса MVP).
Если эти данные хранить в Model, то мы получаем раздутую модель, заботящуюся не о бизнес логике, а о логике отображения.
Если сохранять в Presenter, то нужно реализовать логику совмещения состояния View и состояния Model при инициализации. Дополнительно View должна обрабатывать метод onSaveInstanceState().

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

#Architecture
Что такое MVVM?

MVVM (Model-View-ViewModel) – это шаблон проектирования. Так же как и MVP, MVVM используется для разработки пользовательского интерфейса и разделяет ответственность за бизнес логику и логику отображения.

MVVM состоит из следующих частей:

• Model представляет собой данные, которые необходимо показать пользователю. В большинстве Android-приложений моделью выступает слой, отвечающий за получение данных с бэкэнда.

• View – это класс, отвечающий за отображение данных. В Android-приложениях View – это обычно Activity или Fragment.

• ViewModel – класс, который соединяет View и Model. ViewModel подписана на обновления Model, а View подписана на обновления ViewModel. При этом ViewModel не имеет явной сслыки на View. Подписки реализуются через паттерн Observer.

ViewModel представляет собой маппинг данных, которые отображаются на View.
Допустим, пользователь вводит текст в поле ввода сообщения в приложении-мессенджере и нажимает "Отправить". View изменяет ViewModel. ViewModel выставляет атрибут isMessageRead в значение false и обновляет Model. В нашем примере Model – это абстракция для слоя взаимодействия с бэкендом. Обновление модели значит отправка запроса с введенным сообщением.

Далее собеседник читает сообщение. Model обновляется по сети (например приходит сигнал по сокет-соединению). ViewModel получает оповещение об обновлении Model и обновляет себя, меняя значение атрибута isMessageRead на true. View наблюдает обновление ViewModel и отрисовывает изменение.

#Architecture
Какие существуют способы реализации MVVM Android-приложении?

Суть MVVM и главное отличие от MVP в том, что View наблюдает за обновлениями ViewModel, при этом ViewModel не знает ничего о View.
Такое поведение реализуется через паттерн Observable-Observer и/или связывание данных (data binding).

Существуют следующие способы реализации MVVM в Android:

1. Использование Data Binding библиотеки
Эта библиотека помогает связать ViewModel и UI приложения. ViewModel объявляется как источник данных в лэйаутах соответствующих активити или фрагмента. После этого в элементах лэйаута добавляется связывание с полями ViewModel:

<TextView android:text="@{viewmodel.userName}" />


Библиотека Data Binding генерирует код, который обеспечивает связывание. При обновлении поля userName, обновится текст в TextView.

2. Использование ViewModel и LiveData
ViewModel (класс Architecture Components) реализован так, что переживает пересоздание активити и фрагментов. При использовании этого класса можно не беспокоиться о сохранении состояния, когда изменяется конфигурация.

LiveData – класс, реализующий паттерн Observable. LiveData принимает как наблюдателей объекты, которые реализуют интерфейс LifecycleOwner, например Activity и Fragment. Благодаря этому LiveData автоматически отписывает подписчиков при вызове onDestroy().
Data Binding может использоваться совместно с ViewModel и LiveData.

3. Кастомная реализация
Существует множество способов реализации связи между View и ViewModel, кроме предоставляемых библиотеками от Google.
Например ViewModel может использовать Observable из RxJava и View будет подписываться на обновления ViewModel так же как ViewModel подписывается на обновления Model.
Кастомная реализация добавляет больше гибкости, но нужно самому заботиться о сохранении состояния.

#Architecture
В чем недостатки использования библиотеки 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