Комментарии 84
Однако, утверждение, что все приложения должны быть Single Activity- спорное. Этот подход хорош, когда у Вас типичное «конвейерное приложение» а-ля веб-клиента. Если писать что-то по-сложнее или добавить к существующему Single Activity-приложению действительно тяжелый, с точки зрения ресурсов устройства, функционал- с этого момента могут возникнуть проблемы.
Надо ли говорить, что Single Activity-приложение, чаще всего, представляет собой изобилие рукотворных утечек. Потому работа с таким подходом, если всё делать по совести, вызывает не меньше сложностей в сравнении с multiple activity.
Потому важно иметь права как на болид F1, так и на старый добрый вездеход. И понимать, на чем ехать в конкретном случае. Иногда комбинировать.
Иначе это- просто мода. По типу «asynctask- зло» или «dagger нужен везде». Каждому инструменту- своё применение.
Надо ли говорить, что Single Activity-приложение, чаще всего, представляет собой изобилие рукотворных утечек
Надо :) Почему риск утечек памяти (если вы о них) в Single Activity выше, чем в любом другом подходе?
Если просто- activity, которые есть в backstack, android может выгружать из памяти при ее нехватке, оставляя в памяти только ту, на которою Вы смотрите (и ближайшие к ним), а при возврате восстановить из saved state. Таким образом ресурсы, которые выделены на Ваше приложение, со стороны UI (ну или view/presentation-слоя, смотря что используете) не залочены.
В случае подхода с single activity в backstack все фрагменты держатся в памяти. И то, на что Вы ссылаетесь жесткими ссылками из фрагментов- тоже. Да, в простых случаях это- спички. Но когда в стэке лежит что-то тяжелое, да еще и много инстансов (например, карты, или другие тяжелые элементы/api)- то потребление памяти сильно подскакивает, а вместе с ним падает оперативность и отзывчивость приложения из-за частых вызовов GC и нагрузки в принципе.
Да, Вы можете сказать, что «вот на моем флагмане (или мощном B-бренде) всё хорошо». Однако, часто, бОльшая часть ЦА не располагает достаточно мощными устройствами. И современные low-end устройства с 2-3гб оперативы всё равно испытывают нехватку, и там этот вопрос стоит острее.
Да, конечно, это можно решить\обойти в рамках Single Activity- это вопрос навыков. Однако, по моему опыту, такие решения весьма не тривиальны и многие просто на это забивают.
Если просто- activity, которые есть в backstack, android может выгружать из памяти при ее нехватке, оставляя в памяти только ту, на которою Вы смотрите
Не может. Я об этом специально написал дальше в статье.
Только процесс может быть выгружен из памяти при нехватке ресурсов.
Это будет полноценное отдельное приложение. Там ничего не останется от общего стека и общих расшаренных моделей. Поэтому и разработка этого Activity будет не разработкой экрана, но полноценного приложения. Если мне не изменят память, то там и новый инстанс Application создастся. Я могу ошибаться, такой подход совсем не рядом с обычным стеком экранов, и применяется исключительно для специальных кейсов (когда надо выделить больше памяти на процесс).
А про вендоров — это еще одна из причин делать все в одном Activity, меньше полагаешься на систему, больше работаешь внутри со своими классами.
Это будет полноценное отдельное приложение.Когда будет отдельный проект- то тогда и будет отдельное полноценное приложение. До тех пор у Вас есть логика, которую можно переиспользовать
А откуда эта информация, что отдельное activity из бекстека не может быть выгружено? Этот контракт где-то описан? Я, конечно, глубоко не копал, но при использовании sgs3 даже на глаз было, что activity в различных приложениях при возврате к ним из бекстека иногда пересоздаются.
вызывает не меньше сложностей в сравнении с multiple activity.
сложность это вопрос образованности и опыта, а я попытался доказать, что в "классическом" подходе есть фатальные проблемы, которые не решить сколь угодно сложным кодом
Если покопаться, такие проблемы есть и с single activity. И потому, что оба этих подхода имеют проблемы разного характера, они оба имеют право на существование в продакшене. Что из этого выбирать- вопрос опыта разработчика.
Однако, согласитесь, что заголовок и некоторые доводы из статьи весьма провокационны. Вы сравниваете умение применять повсеместно single activity с правами F1, что, всё-таки, расходится с реальностью. Я прочитал статью с удовольствием, но такие моменты смущают.
Лично я (как, думаю, и многие) вижу это так- «Либо ты пишешь только single activity, либо ты- плохой разраб»
И ладно, если в это верят отдельные разрабы- это вопрос доверчивости и опыта каждого. Но, читая подобные статьи на хабре, в это начинает верить техническое руководство. А это- уже проблема…
Если писать что-то по-сложнее...
Пока это просто неподкрепленное фактами утверждение. Пример: приложение Uber с 1700+ градл модулями в проде написано целиком в одном Activity. Как вы считаете это тоже "типичное «конвейерное приложение»"?
Приведите пример, который на нескольких Activity решается бесспорно лучше, и мы детально обсудим)
приложение Uber с 1700+ градл модулямиНа мой скромный взгляд, Uber не является хорошим примером приложения. Как и многие от крупных брендов.
Примеры- сервисы синхронизации, аутентикаторы, камеры различной насыщенности с кучей точек входа, приложения с широким функционалом шаринга, чатики с быстрым ответом, навороченные радио и плееры… В общем примеров много, и перечислил далеко не все. В основном те приложения, которые часто контактируют с пользователем посредством точек входа ОС, нежели напрямую
Для этого вы должны были обратить особое внимание на самую последнюю часть в статье!
Какой же это тогда single activity?
Поймите правильно, подавляющее большинство задач можно реализовать обоими путями, но не надо позиционировать свой подход как «истинно верный»…
На мой взгляд, надо брать то, что дает меньше сопротивления в каждом конкретном случае
Какой же это тогда single activity?
Вот поэтому я очень просил в статье внимательно читать последний абзац.
Это надо уметь сохранить single-activity.
"дает меньше сопротивления" — и перечитав недостатки нескольких Activity в приложении надо все писать в одном. И принять подход как "«истинно верный»" :-)
узнать высоту клавиатуры (это реально!)
а в рамках вашей статьи не показали пример
Потому что статья не об этом, но там специально выделено: Всем изучать WindowInsets!
Когда появляется клавиатура, то система сообщает ее размер через применение WindowInsets к Activity, именно так и можно узнать ее высоту.
Спасибо за статью!
Пара вопросов:
1) Темы фрагментам задаёте в onCreateView, создавая ContextThemeWrapper? Что-то вроде:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val contextThemeWrapper = ContextThemeWrapper(activity, R.style.Theme_MyApp_Dark)
val themedInflater = inflater.cloneInContext(contextThemeWrapper)
return themedInflater.inflate(R.layout.fragment_smoething_dark, container, false)
}
?
2) Передача данных с экрана на экран. Допустим, у нас в приложении есть экран, который умеет сканировать паспорт, и возвращать объект с паспортными данными. Если это Activity, то всё понятно, у нас одна точка выхода: onActivityResult(). Перегрузили, распарсили, передали куда надо.
Как правильно сделать это через фрагмент? Можете чуть подробнее рассказать о подходе к реализации "реактивного подхода", который использует ваша команда?
1) Нет, просто в разметке
2) два варианта:
- если этот сканер нужен только нам, то в него будет инжектиться модель, внутри которой он проставит отсканированные данные. а сама модель через коллбек уведомит тех, кому это важно
- если этот сканер переиспользуется во многих проектах и мы хотим отвязать его логику данных от логики проекта, то результат будет возвращен контейнеру через нужный интерфейс, а контейнер отправит данные в заинжекченную модель и так далее.
Можно повесить тему непосредственно, на root элемент в layout'е. И это будет даже лучше, чем указывать это в манифесте, т.к. всё что касается отображения конкретного окна будет в одном месте.
Более интересные вопрос что делать с штуками типа windowBackground. Тут видимо придётся либо использовать один background на всё приложение, либо в каждом фрагменте указывать фон явно.
про windowBackground я отдельно сказал в разделе про Splash Screen
если такая проблема есть (нужен постоянно новый фон), то можно в рантайме менять фон окна, но лучше конечно поговорить с дизайнерами о консистентности экранов
узнать правильную высоту статус бара (а не хардкодить 51dp)
А разве не 24dp все хардкодят? 51 — как-то много для статусбара :)
Вообще, кейс очень даже реальный. Успели с ним столкнуться. Несмотря на то, что на material.io написано, что высота statusbar должна быть 24 dp, по-факту, она может быть какой угодно. Особенно это заметно на девайсах с "челкой".
Don't keep activities
Этот флаг для тестирования действительно позволяет найти некоторые баги в приложении, но поведение, которое он воспроизводит, НИКОГДА не встречается в реальности!
У нас последнее время стали встречаться все чаще, так как вендоры все больше перепиливают систему. Возможно у вас не получилось это воспроизвести, но это не значит, что проблемы нет)
Don't keep Activities воспроизводит следующее поведение:
- Свернули приложение
- Запустили другое ресурсоемкое и/или дали телефону уйти в сон
- Развернули свое приложение
Поднимется верхнее Activity стэка, при этом нижние будут задестроены.
Собственно, поэтому дико плюсую за подход с Single Activity при шаренной неперсистентной бизнес логике. Однако, в некоторых случаях можно использовать и отдельное Activity, например, для инициализации нативных библиотек, камеры, VR и т.д.
Именно! Это то, о чем я и говорю) Процесс в вашем кейсе тоже будет пересоздан!
Только вместе с процессом могут умирать Activity, никогда не может быть такого, что вы находитесь в приложении на некотором экране, а в этот момент другие экраны (Activity) выгружаются. И именно такое поведение создает Don't keep Activities. Посмотрите внимательно в статью.
Правильное поведение для тестирование обеспечивает другая настройка в дебаг меню: количество фоновых процессов. Если выставить в 1, то ваше приложение будет сразу при сворачивании выгружаться системой.
Другой случай — это поворот приложения. Там Activity будут лениво пересоздаваться при возврате по стеку назад.
для инициализации нативных библиотек, камеры, VR и т.д.
Это еще одна вариация самого последней части в статье. Я для этого пишу отдельное "приложение", то есть Activity никак не связанное с основным приложением. Там нет общего слоя данных, общих стилей, общих моделек — ничего. И общаюсь с ним только через Intent.
Но этот кейс редкий и поэтому такой особенный.
android:windowSoftInputMode=«adjustResize» — установили
Если вы используете для обработки прозрачного статус-бара подход из прошлого раздела, то обнаружите досадную ошибку: если фрагмент успешно «подлезал» под статус бар, то при появлении клавиатуры он сожмется сверху и снизу, так как и статус бар и клавиатура внутри системы работают через SystemWindows.
т.е на анимации, что вы показали при сворачивании клавиатуры заголовок не переместился в первоначальное положение:
для решения проблемы надо переопределить onApplyWindowInsets и вызывать dispatchApplyWindowInsets у child?
Перевести-то вы GitFox на single-activity перевели, вот только приложение лишилось анимаций и появились баги с бэкстеком вроде белого экрана при нажатии кнопки назад на первом экране. И это как бы намекает, что подход не идеален.
Вы не правы. Путаете мягкое с теплым.
Белый экран — это был косяк в коде, а не подхода.
Анимаций и не было, то что ваша системы сама подставляла что-то на переключение Activity — это не задумка разработчиков, поэтому хорошо, что сейчас все ведет себя как ожидалось, а не как захотелось вендору
Суть моего комментария вот в чем — раз уж вы приводите пример приложения в качестве иллюстрации замечательности вашего подхода, то уж позаботьтесь о том, чтобы оно работало хотя бы не хуже варианта с несколькими Activity, иначе преимущества для пользователя неочевидны.
У меня была проблема, которую не получилось нормально решить в рамках одной активити: по нажатию на превью картинки, должна проиграться анимация перехода картинки на фулскрин, а по свайпу картинка улетает обратно. Такое поведение есть в настройках телеграмма, когда нажимаешь на свою аватарку. Дело в том, что транзишн на фрагментах не работает, если вызывать метод add, только replace или remove, а поскольку нам нужно, чтобы под полноэкранной картинкой оставался прошлый фрагмент, нам такой вариант не подходит. Есть вариант сделать превьюху в чайлд фрагменте, внутри основного и реплейсить на фуллскрин франмент, тогда анимация работать будет, но если превьюха находится внутри CollapsingToolbarLayout, то контейнер с превьюхой не получиться сделать во весь экран и реплейс будет происходить не так, как мы задумали. В итоге не придумал ничего лучше кроме как использовать отдельную активити для фулскрин галереи. Думаю, в данном случае это нормально, но интересно, можно ли эту задачу решить без дополнительной активити и без создания анимаций вручную.
Удивительно, что кто-то еще не пишет приложения в Single-Activity. В любой момент может потребоваться поместить экран в NavigationDrawer, ViewPager или BottomBar. В этом случае без кучи фрагментов в одной Activity не обойтись.
Интересно получается, что мейнстрим пошел по пути кроссплатформенных фреймворков (например Qt), когда все внутренние страницы приложения живут внутри одной системной Activity. Чем меньше зависимость от поведения системы, тем меньше головной боли, однако каждый пилит свои реализации и user experience в одинаковых задачах отличается. И столько библиотечных возможностей пылится…
С фрагентами конечно проще, но в целом писать одно мучение.
Будем реалистами, зайдем на гитлаб GitFox и посмотрим статистику, сотни комитов, несколько авторов. Установим приложение (на первом же экране кнопка back ведет на пустое место, ошибка в токене никак не сообщается юзеру, просто слетает форма и все), по сути это клиент для части гитлабовского рест апи. На каком-нибудь реакте или ионике или том же новом флаттере это все заняло бы максимум неделю времени: сделать апи клиент, закодить n экранов/списков, без лишних размышлений о потоках, активити, лайфсайклах, листвью, темах, стилях и тп.
так давайте! сделайте ваш открытый клиент для гитлаба за пару недель! сообщество только спасибо скажет, главное не обращайте внимание, если вам скажут, что ваш безвозмездный труд — фигня, а можно за пару дней нормально сделать.
кто ж вам мешает? кидайте ссылку на проект через недельку другую
Немного не понятен один вопрос. Предположим мы берем приложуху с не залоченным экраном, а нормальное полностью адаптивное приложене, обрабатывающие повороты и разные размеры и все что только нужно. Вы предлагаете использовать SinglActivity и увязывать ее жизненный цикл с жизненным циклом приложения. Если я правильно Вас понял, то такой подход возможен только при android:configChanges=«orientation», в противном случае наша активити при повороте будет пересоздаваться.
Резюмируя — для адаптивных приложении Вы советуете использовать android:configChanges=«orientation»?
Нет, вы неправильно поняли. Прошу, перечитайте еще раз. Там прямо про поворот экрана отмечено "Вот верное решение" и так далее
override fun onResume() {
super.onResume()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
override fun onPause() {
super.onPause()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
Это предложение справедливо для приложения которое в зависимости от экрана либо обрабатывает либо нет поворот.
А я все же говорю, что мы априори обрабатываем повороты всех экранов всегда.
т.е. мы все же берем как описано у Вас в манифесте
<activity android:name=".AppActivity"
android:configChanges=«orientation» />
нам же не нужно устанавливать в моем случае флаги SCREEN_ORIENTATION_PORTRAIT и SCREEN_ORIENTATION_SENSOR
Зачем тогда вообще блокировать поворот? Обрабатываете и все.
Не понимаю..
Просто сейчас реализую Single Activity App со всеми поворотами, но в моем проекте она все же пересоздается при поворотах т.е. параметра android:configChanges в манифесте нет.
А, я понял, хороший вопрос. Ответ такой: в Activity есть системный флаг isFinishing(), который говорит о том срабатывают коллбеки ЖЦ из-за завершения экрана или это просто поворот (пересоздание).
Буду разбираться.
retained фрагменты как работали, так и работают. мы их правда не используем, достаточно Toothpick с простым управлением DI-скоупами
По сути retain позволяет сохранять данные во фрагменте но все равно пересоздает UI. А если мы храним данные в другом слое необходимости в этом нет.
Если только мы не привязываем один хостовой фрвгмент к ЖЦ приложения для обработки чего либо ранее бывшего в application))(но я так не делал)
Вообще странно, что по умолчанию GOOGL не сделал активити android:configChanges=«orientation» а фрагмент retain, в противном случае это похоже уже на костыль против порочной логики пересоздания. Хотя я могу заблуждаться
Вообще странно, что по умолчанию GOOGL не сделал активити android:configChanges=«orientation» а фрагмент retainДа нет в этом ничего странного. configChanges=«orientation» не стоит по дефолту, т.к. могут быть разные layout'ы для разных ориентаций. А фрагменты не retained, потому что содержат UI и должны пересоздаваться по такими же правилам, как и activity.
Не понимаю, как при configChanges=«orientation» вы хотите не пересоздать Activity, а только повернуть верстку? Мы обязаны вызвать super метод, а он пересоздаст все.
А отвечал я комментатору выше, почему configChanges=«orientation» не стоит по-умолчанию.
android:configChanges=«orientation|screenSize»
удивительно что вы это пропустили.
Да, вы полностью правы. Что здесь удивительного, все где-то ошибаются :)
Тем не менее я не советую не учитывать пересоздание активити, так как кроме этих случаев, есть множество других, когда это потребуется. Поэтому лучше не надеяться на меньшее, а делать сразу правильно.
В редких случаях это действительно полезно, например, экран с вебвью или картой, так как они долго создаются и инициализируются.
Дополнил статью.
конечно такое поведение может не подойти некоторым приложениям где для разных ориентаций разные макеты, но при желании смену вьюшек\их расположения можно наколхозить и в onConfigurationChanged
Так, а чем тогда вам android:screenOrientation="portrait" не подходит? Если верстка не меняется, то и поворачивать не надо
В моем случае при повороте текущие вьюшки не пересоздаются, а просто меняют свои размеры и положения в соответствии с новым размером экрана.
пример: в приложениях Telegram/WhatsApp на экране списка чатов как раз этот случай — иерархия\layoutParams не меняются, а вьюшки растягиваются при повороте экрана.
Да, но тут стоит учесть, что во многих приложениях используется Toolbar, а его высота берется системой из ?attr/actionBarSize, которая, в свою очередь, достается из системных параметров, зависящих от ориентации. Поэтому Toolbar в ландшафтной ориентации узкий.
Если сделать android:configChanges=«orientation|screenSize», то пересоздания не будет
Добавлю свои 5 коп.
Передача результата между экранами
Еще вариант: гугл рекомендует в AAC для этого использовать AAC ViewModel.
Один из функционалов — это встроенный крутой просмотрщик картинок из чатов.
Вы находитесь сейчас НЕ в приложении, а, например, в браузере, приложение свернуто, сокета нет и тд. Теперь приходит пуш нотификация, что пользователь прислал вам картинку. Вы хотите по открыть свой крутой просмоторщик картинок прямо из пуша — благо там есть вся информация — url картинки, кто прислал. При этом по беку надо вернуться именно туда, откуда пришли — в браузер.
Вызывать single activity, поднимать сокет, прокидывать на экран, и кастомно обрабатывать back? Или все-таки просмоторщик картинок завернуть в отдельную активити, которую и вызвать из pending intent при клике на пуш?
В самой статье есть похожий пример, про использование из других приложений, тут почти тоже самое, только ведь по сути это не совсем «чужое» приложение.
Самое противное при single activity это если есть большое кол-во фрагментов, получается большая стейтмашина. Но эту проблему может решить рано или поздно google, например с navigation component в AAC.
Этот случай я описал подробно в последнем абзаце.
Зачем вам открывать сокет, если вы открыли просмотрщик из нотификации и хотите сразу выйти по кнопке назад?
Сделайте фрагмент-просмотрщик. В основном приложении открывайте его в активити где все остальные экраны.
А для запуска извне открывайте SpecialActivity, которое будет переиспользовать ваш фрагмент-просмотрщик для внешних вызовов.
Лицензия на вождение болида, или почему приложения должны быть Single-Activity